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
+192
View File
@@ -7601,6 +7601,198 @@ document.addEventListener('DOMContentLoaded', () => {
let dragStartWidth, dragStartHeight; let dragStartWidth, dragStartHeight;
let activeAssetTab = 'postit'; let activeAssetTab = 'postit';
// --- ESTÚDIO DE CENAS EM CAMADAS ---
const btnDecomposeScene = document.getElementById('btnDecomposeScene');
const sceneDecomposePrompt = document.getElementById('sceneDecomposePrompt');
const sceneDecomposeLoader = document.getElementById('sceneDecomposeLoader');
const sceneDecomposeStatus = document.getElementById('sceneDecomposeStatus');
const sceneLayersList = document.getElementById('sceneLayersList');
const sceneLayersItems = document.getElementById('sceneLayersItems');
// Cria um item de camada de imagem direto no canvas (background ou sticker)
function addSceneLayerToCanvas(layerData) {
if (!canvasModeActive || !cartazPrintableArea) return;
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.position = 'absolute';
item.style.left = (layerData.left || 10) + '%';
item.style.top = (layerData.top || 10) + '%';
item.style.width = (layerData.width || 35) + '%';
item.style.height = (layerData.height || 35) + '%';
item.style.zIndex = layerData.isBackground ? '1' : String(10 + (layerData.zOffset || 0));
item.style.background = 'transparent';
if (layerData.isBackground) {
// Fundo: cobrindo 100% e não selecionável como item de controle
item.style.left = '0';
item.style.top = '0';
item.style.width = '100%';
item.style.height = '100%';
item.style.zIndex = '0';
item.style.pointerEvents = 'none'; // Por padrão, o fundo não captura eventos para facilitar a seleção dos elementos acima
item.innerHTML = `<img src="${layerData.url}"
style="width:100%; height:100%; object-fit:cover; pointer-events:none; display:block;">`;
item.title = '🖼️ Cenário de Fundo';
} else {
item.innerHTML = `<img src="${layerData.url}"
style="width:100%; height:100%; object-fit:contain; pointer-events:none;">`;
item.title = '🎬 ' + (layerData.name || 'Elemento');
}
const handle = document.createElement('div');
handle.className = 'canvas-resize-handle';
item.appendChild(handle);
cartazPrintableArea.appendChild(item);
if (!layerData.isBackground) {
setupCanvasItemEvents(item);
// Permitir que o fundo seja selecionável após clicar nele diretamente
} else {
// para o fundo, adicionar capacidade de clicar para habilitar arrastar
item.style.pointerEvents = 'auto';
setupCanvasItemEvents(item);
}
return item;
}
// Renderiza a lista de camadas no painel lateral
function renderSceneLayers(layers) {
if (!sceneLayersItems) return;
sceneLayersItems.innerHTML = '';
const allLayers = [{ name: '🖼️ Cenário de Fundo', isBackground: true }, ...layers.map(l => ({ ...l }))];
allLayers.forEach((layer, index) => {
const chip = document.createElement('div');
chip.style.cssText = `display:flex; align-items:center; gap:8px; padding:5px 8px;
background:rgba(255,255,255,0.05); border:1px solid rgba(139,92,246,0.2);
border-radius:6px; font-size:0.78rem; color:var(--text-primary); cursor:pointer;`;
chip.innerHTML = `
<span style="font-size:1rem;">${layer.isBackground ? '🌄' : '✨'}</span>
<span style="flex:1;">${layer.name}</span>
<span style="font-size:0.68rem; color:var(--text-secondary);">Camada ${index}</span>
`;
chip.title = `${layer.isBackground ? 'Fundo da cena' : 'Elemento: ' + layer.name}`;
sceneLayersItems.appendChild(chip);
});
}
if (btnDecomposeScene) {
btnDecomposeScene.addEventListener('click', async () => {
const prompt = sceneDecomposePrompt ? sceneDecomposePrompt.value.trim() : '';
if (!prompt) {
showToast('Descreva a cena antes de gerar!', 'warning');
return;
}
// Garantir que o modo canvas está ativo
if (!canvasModeActive) {
if (cartazEditArea && cartazEditArea.style.display !== 'none') {
btnEditCartazToggle && btnEditCartazToggle.click();
}
enterCanvasMode();
}
// Mostrar loading
btnDecomposeScene.disabled = true;
btnDecomposeScene.style.opacity = '0.6';
if (sceneDecomposeLoader) {
sceneDecomposeLoader.style.display = 'flex';
}
if (sceneLayersList) sceneLayersList.style.display = 'none';
const statusSteps = [
'Planejando a cena com IA...',
'Identificando elementos e cenário...',
'Gerando imagem de fundo...',
'Gerando elementos individuais...',
'Montando as camadas no canvas...'
];
let stepIdx = 0;
const statusInterval = setInterval(() => {
if (sceneDecomposeStatus && stepIdx < statusSteps.length) {
sceneDecomposeStatus.textContent = statusSteps[stepIdx++];
}
}, 3500);
try {
const titulo = activeCartazData?.titulo || '';
const response = await fetch('/api/cartazes/decompose-scene', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({ tema: prompt, titulo })
});
clearInterval(statusInterval);
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...';
// Limpar canvas antes de montar (remover outros items de cena se houver)
const oldLayers = cartazPrintableArea.querySelectorAll('.canvas-scene-layer');
oldLayers.forEach(l => l.remove());
// Adicionar background
if (data.backgroundUrl) {
addSceneLayerToCanvas({ url: data.backgroundUrl, name: 'Cenário de Fundo', isBackground: true });
}
// Adicionar cada elemento com posição escalonada
if (Array.isArray(data.layers)) {
data.layers.forEach((layer, idx) => {
if (layer.url) {
const spread = [
{ left: 10, top: 30 },
{ left: 38, top: 35 },
{ left: 62, top: 28 },
{ left: 25, top: 55 }
];
const pos = spread[idx % spread.length];
addSceneLayerToCanvas({
url: layer.url,
name: layer.name || `Elemento ${idx + 1}`,
isBackground: false,
left: pos.left,
top: pos.top,
width: 28,
height: 28,
zOffset: idx + 1
});
}
});
// Renderizar lista de camadas no painel
renderSceneLayers(data.layers);
if (sceneLayersList) sceneLayersList.style.display = 'flex';
}
showToast('🎬 Cena montada em camadas! Clique nos elementos para mover, girar e redimensionar.', 'success');
} catch (err) {
clearInterval(statusInterval);
console.error('[Decompose Scene]', err);
showToast('Erro ao criar a cena em camadas: ' + err.message, 'error');
} finally {
btnDecomposeScene.disabled = false;
btnDecomposeScene.style.opacity = '1';
if (sceneDecomposeLoader) sceneDecomposeLoader.style.display = 'none';
}
});
}
function enterCanvasMode() { function enterCanvasMode() {
canvasModeActive = true; canvasModeActive = true;
if (btnCanvasModeToggle) { if (btnCanvasModeToggle) {
+34 -5
View File
@@ -1330,7 +1330,7 @@
<!-- GERENCIADOR DE PROJETOS --> <!-- GERENCIADOR DE PROJETOS -->
<div style="background: rgba(255,255,255,0.03); border: 1px solid var(--border-light); padding: 14px; border-radius: 12px; display: flex; flex-direction: column; gap: 10px;"> <div style="background: rgba(255,255,255,0.03); border: 1px solid var(--border-light); padding: 14px; border-radius: 12px; display: flex; flex-direction: column; gap: 10px;">
<label style="color: var(--text-secondary); font-size: 0.82rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">📁 Meus Projetos de Quadrinhos</label> <label style="color: var(--text-secondary); font-size: 0.82rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">📁 Meus Projetos de Quadrinhos</label>
<div style="display: flex; gap: 10px; align-items: center;"> <div class="comics-project-row" style="display: flex; gap: 10px; align-items: center;">
<select id="comicsProjectSelect" class="obs-select" style="flex: 1; padding: 8px 12px; font-size: 0.9rem;"> <select id="comicsProjectSelect" class="obs-select" style="flex: 1; padding: 8px 12px; font-size: 0.9rem;">
<option value="">-- Carregar projeto salvo... --</option> <option value="">-- Carregar projeto salvo... --</option>
</select> </select>
@@ -1338,7 +1338,7 @@
<button type="button" id="btnDeleteComicsProject" class="btn-music-option" style="padding: 8px 14px; font-size: 0.82rem; border-color: #ef4444; color: #ef4444; background: rgba(239, 68, 68, 0.05); margin: 0; white-space: nowrap; display: none;">🗑️ Excluir</button> <button type="button" id="btnDeleteComicsProject" class="btn-music-option" style="padding: 8px 14px; font-size: 0.82rem; border-color: #ef4444; color: #ef4444; background: rgba(239, 68, 68, 0.05); margin: 0; white-space: nowrap; display: none;">🗑️ Excluir</button>
</div> </div>
<div style="display: flex; align-items: center; gap: 10px;"> <div class="comics-project-row" style="display: flex; align-items: center; gap: 10px;">
<input type="text" id="comicsProjectTitleInput" class="obs-input" placeholder="Título do Projeto (Ex: Lucas e Mariana no Parque)" style="flex: 1; margin: 0; padding: 8px 12px; font-size: 0.9rem;"> <input type="text" id="comicsProjectTitleInput" class="obs-input" placeholder="Título do Projeto (Ex: Lucas e Mariana no Parque)" style="flex: 1; margin: 0; padding: 8px 12px; font-size: 0.9rem;">
<button type="button" id="btnSaveComicsProject" class="btn-music-option" style="padding: 8px 14px; font-size: 0.82rem; border-color: #10a37f; color: white; background: #10a37f; margin: 0; white-space: nowrap; font-weight: 600; display: none;">💾 Salvar Alterações</button> <button type="button" id="btnSaveComicsProject" class="btn-music-option" style="padding: 8px 14px; font-size: 0.82rem; border-color: #10a37f; color: white; background: #10a37f; margin: 0; white-space: nowrap; font-weight: 600; display: none;">💾 Salvar Alterações</button>
<button type="button" id="btnSaveNewComicsProject" class="btn-music-option" style="padding: 8px 14px; font-size: 0.82rem; border-color: var(--brand-pink); color: white; background: var(--brand-pink); margin: 0; white-space: nowrap; font-weight: 600;">💾 Salvar como Novo</button> <button type="button" id="btnSaveNewComicsProject" class="btn-music-option" style="padding: 8px 14px; font-size: 0.82rem; border-color: var(--brand-pink); color: white; background: var(--brand-pink); margin: 0; white-space: nowrap; font-weight: 600;">💾 Salvar como Novo</button>
@@ -1882,6 +1882,35 @@
<!-- Lado Esquerdo: Painel de Assets para Canvas --> <!-- Lado Esquerdo: Painel de Assets para Canvas -->
<div id="cartazLeftCanvasPanel" style="display: none; flex: 1; min-width: 320px; flex-direction: column; gap: 16px; padding-right: 12px;"> <div id="cartazLeftCanvasPanel" style="display: none; flex: 1; min-width: 320px; flex-direction: column; gap: 16px; padding-right: 12px;">
<h4 style="margin: 0; color: var(--text-primary); border-bottom: 1px dashed var(--border-light); padding-bottom: 8px;">🎨 Elementos do Canvas</h4> <h4 style="margin: 0; color: var(--text-primary); border-bottom: 1px dashed var(--border-light); padding-bottom: 8px;">🎨 Elementos do Canvas</h4>
<!-- 🎬 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;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 1.3rem;">🎬</span>
<div>
<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>
</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>
</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;">
<div class="loading-dot" style="width:8px;height:8px;border-radius:50%;background:#8b5cf6;animation:dot-pulse 1.2s ease-in-out 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.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>
</div>
<div class="settings-group"> <div class="settings-group">
<input type="text" id="canvasAssetSearch" placeholder="🔍 Buscar elementos..." class="obs-input" style="width: 100%; padding: 10px;"> <input type="text" id="canvasAssetSearch" placeholder="🔍 Buscar elementos..." class="obs-input" style="width: 100%; padding: 10px;">
@@ -1901,10 +1930,10 @@
</div> </div>
<!-- Lado Direito: Preview & Editor de Código --> <!-- Lado Direito: Preview & Editor de Código -->
<div style="flex: 1.3; min-width: 360px; display: flex; flex-direction: column; gap: 14px;"> <div class="cartaz-preview-panel" style="flex: 1.3; min-width: 360px; display: flex; flex-direction: column; gap: 14px;">
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-light); padding-bottom: 8px;"> <div class="cartaz-header-container" style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-light); padding-bottom: 8px;">
<h4 style="margin: 0; color: var(--text-primary);">👁️ Visualização do Cartaz</h4> <h4 style="margin: 0; color: var(--text-primary);">👁️ Visualização do Cartaz</h4>
<div style="display: flex; gap: 8px;"> <div class="cartaz-header-actions" style="display: flex; gap: 8px;">
<button id="btnCanvasModeToggle" class="btn-save-settings" style="background: transparent; border: 1px solid var(--border-light); color: var(--text-secondary); padding: 5px 10px; font-size: 0.75rem;">🎨 Design Canvas</button> <button id="btnCanvasModeToggle" class="btn-save-settings" style="background: transparent; border: 1px solid var(--border-light); color: var(--text-secondary); padding: 5px 10px; font-size: 0.75rem;">🎨 Design Canvas</button>
<button id="btnEditCartazToggle" class="btn-save-settings" style="background: transparent; border: 1px solid var(--border-light); color: var(--text-secondary); padding: 5px 10px; font-size: 0.75rem;">✏️ Editar HTML</button> <button id="btnEditCartazToggle" class="btn-save-settings" style="background: transparent; border: 1px solid var(--border-light); color: var(--text-secondary); padding: 5px 10px; font-size: 0.75rem;">✏️ Editar HTML</button>
<button id="btnPrintCartaz" class="btn-save-settings" style="background: #3b82f6; border: none; color: white; padding: 5px 12px; font-size: 0.75rem; font-weight: bold; border-radius: 6px;">🖨️ Imprimir Cartaz</button> <button id="btnPrintCartaz" class="btn-save-settings" style="background: #3b82f6; border: none; color: white; padding: 5px 12px; font-size: 0.75rem; font-weight: bold; border-radius: 6px;">🖨️ Imprimir Cartaz</button>
+120
View File
@@ -1555,6 +1555,84 @@ code {
padding-bottom: 16px !important; padding-bottom: 16px !important;
} }
#minhaTurmaModal .settings-modal-body {
overflow-y: auto !important;
flex: none !important;
min-height: auto !important;
max-height: calc(92dvh - 120px) !important;
}
#turmaConfigSection, #turmaAlunosSection {
flex-direction: column !important;
overflow-y: visible !important;
height: auto !important;
}
#turmaConfigSection > div, #turmaAlunosSection > div {
border-right: none !important;
border-bottom: 1px solid var(--border-light) !important;
padding-right: 0 !important;
padding-bottom: 16px !important;
flex: none !important;
height: auto !important;
width: 100% !important;
min-width: 0 !important;
}
#turmaConfigSection > div:last-child, #turmaAlunosSection > div:last-child {
border-bottom: none !important;
padding-bottom: 0 !important;
padding-left: 0 !important;
}
#turmaConfigSection select, #turmaConfigSection input,
#turmaAlunosSection select, #turmaAlunosSection input {
width: 100% !important;
}
.comics-project-row {
flex-direction: column !important;
align-items: stretch !important;
gap: 8px !important;
}
.comics-project-row select,
.comics-project-row input,
.comics-project-row button {
width: 100% !important;
margin: 0 !important;
}
.cartaz-preview-panel {
min-width: 0 !important;
width: 100% !important;
flex: none !important;
}
#cartazLeftConfigPanel, #cartazLeftCanvasPanel {
min-width: 0 !important;
width: 100% !important;
padding-right: 0 !important;
flex: none !important;
}
.cartaz-header-container {
flex-direction: column !important;
align-items: flex-start !important;
gap: 10px !important;
}
.cartaz-header-actions {
flex-wrap: wrap !important;
width: 100% !important;
justify-content: flex-start !important;
}
.cartaz-header-actions button {
flex: 1 1 auto !important;
text-align: center !important;
}
#modelosRelatorioModal .settings-modal-body > div:last-child { #modelosRelatorioModal .settings-modal-body > div:last-child {
padding-left: 0 !important; padding-left: 0 !important;
padding-top: 16px !important; padding-top: 16px !important;
@@ -3739,6 +3817,48 @@ code {
} }
} }
/* Camadas de Cena (Estúdio de Cenas em Camadas) */
.canvas-scene-layer {
padding: 0 !important;
background: transparent !important;
border-radius: 0 !important;
overflow: hidden;
}
.canvas-scene-layer:hover {
border-color: rgba(139, 92, 246, 0.8);
box-shadow: 0 0 10px rgba(139, 92, 246, 0.3);
}
.canvas-scene-layer.active-item {
border: 2.5px solid #8b5cf6 !important;
box-shadow: 0 0 14px rgba(139, 92, 246, 0.5);
}
/* Animação dos dots de loading */
@keyframes dot-pulse {
0%, 60%, 100% { transform: scale(1); opacity: 0.5; }
30% { transform: scale(1.4); opacity: 1; }
}
/* Botão Estúdio de Cenas */
#btnDecomposeScene:hover:not(:disabled) {
opacity: 0.9;
transform: translateY(-1px);
box-shadow: 0 4px 15px rgba(139, 92, 246, 0.4);
}
#btnDecomposeScene:disabled {
cursor: not-allowed;
}
/* Lista de camadas no painel */
#sceneLayersItems > div:hover {
background: rgba(139, 92, 246, 0.12) !important;
border-color: rgba(139, 92, 246, 0.4) !important;
}
+4 -2
View File
@@ -64,8 +64,9 @@ self.addEventListener('fetch', (event) => {
return fetch(event.request).then((networkResponse) => { return fetch(event.request).then((networkResponse) => {
if (networkResponse && networkResponse.status === 200) { if (networkResponse && networkResponse.status === 200) {
const responseToCache = networkResponse.clone();
caches.open(CACHE_NAME).then((cache) => { caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, networkResponse.clone()); cache.put(event.request, responseToCache);
}); });
} }
return networkResponse; return networkResponse;
@@ -78,8 +79,9 @@ self.addEventListener('fetch', (event) => {
fetch(event.request) fetch(event.request)
.then((networkResponse) => { .then((networkResponse) => {
if (networkResponse && networkResponse.status === 200) { if (networkResponse && networkResponse.status === 200) {
const responseToCache = networkResponse.clone();
caches.open(CACHE_NAME).then((cache) => { caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, networkResponse.clone()); cache.put(event.request, responseToCache);
}); });
} }
return networkResponse; return networkResponse;
+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) => { app.get('/api/cartazes/list', requireAuth, async (req, res) => {
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000'; const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
try { try {