diff --git a/public/app.js b/public/app.js index b1f29ee..fe02208 100644 --- a/public/app.js +++ b/public/app.js @@ -3451,6 +3451,475 @@ const initApp = () => { } }); } + + // ============================================================ + // FÁBRICA DE QUADRINHOS (Criação de histórias visuais pedagógicas) + // ============================================================ + const fabricaQuadrinhosModal = document.getElementById('fabricaQuadrinhosModal'); + const btnCloseComicsModal = document.getElementById('btnCloseComicsModal'); + const barBtnComics = document.getElementById('barBtnComics'); + + const btnGenerateComics = document.getElementById('btnGenerateComics'); + const comicsTemaInput = document.getElementById('comicsTemaInput'); + const comicsGenerateLoader = document.getElementById('comicsGenerateLoader'); + const comicsLoaderText = document.getElementById('comicsLoaderText'); + const comicsLoaderProgress = document.getElementById('comicsLoaderProgress'); + + const comicsResultArea = document.getElementById('comicsResultArea'); + const comicsResultTitle = document.getElementById('comicsResultTitle'); + const comicsGridContainer = document.getElementById('comicsGridContainer'); + + const comicsCenarioSelect = document.getElementById('comicsCenarioSelect'); + const comicsCenarioCustomInput = document.getElementById('comicsCenarioCustomInput'); + + // Export PDF / Vídeo + const btnExportComicsPDFPortrait = document.getElementById('btnExportComicsPDFPortrait'); + const btnExportComicsPDFLandscape = document.getElementById('btnExportComicsPDFLandscape'); + const btnCompileComicsVideo = document.getElementById('btnCompileComicsVideo'); + const comicsVideoMusic = document.getElementById('comicsVideoMusic'); + const comicsVideoDuration = document.getElementById('comicsVideoDuration'); + const comicsVideoLoader = document.getElementById('comicsVideoLoader'); + const comicsVideoResult = document.getElementById('comicsVideoResult'); + const comicsVideoPlayer = document.getElementById('comicsVideoPlayer'); + const comicsVideoDownloadBtn = document.getElementById('comicsVideoDownloadBtn'); + + let selectedComicsQty = 5; + let selectedComicsBubbles = true; + let selectedComicsRatio = '16:9'; + let generatedPanelsData = []; + + // Toggle ativo quantidade de quadros + document.querySelectorAll('.btn-comics-qty').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('.btn-comics-qty').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + selectedComicsQty = parseInt(btn.dataset.qty); + }); + }); + + // Toggle ativo falas/diálogos + document.querySelectorAll('.btn-comics-bubbles').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('.btn-comics-bubbles').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + selectedComicsBubbles = btn.dataset.bubbles === 'true'; + }); + }); + + // Toggle ativo proporção visual + document.querySelectorAll('.btn-comics-ratio').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('.btn-comics-ratio').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + selectedComicsRatio = btn.dataset.ratio; + }); + }); + + // Toggle ativo personagens principais + document.querySelectorAll('.btn-comics-char').forEach(btn => { + btn.addEventListener('click', () => { + btn.classList.toggle('active'); + }); + }); + + // Exibir/ocultar cenário personalizado + if (comicsCenarioSelect) { + comicsCenarioSelect.addEventListener('change', () => { + if (comicsCenarioSelect.value === 'custom') { + comicsCenarioCustomInput.style.display = 'block'; + } else { + comicsCenarioCustomInput.style.display = 'none'; + } + }); + } + + // Abrir e fechar modal Fábrica de Quadrinhos + const openComicsModal = () => { + fabricaQuadrinhosModal.style.display = 'flex'; + comicsTemaInput.value = ''; + comicsResultArea.style.display = 'none'; + comicsGenerateLoader.style.display = 'none'; + comicsVideoResult.style.display = 'none'; + comicsVideoLoader.style.display = 'none'; + btnGenerateComics.disabled = false; + comicsLoaderProgress.style.width = '0%'; + }; + + if (barBtnComics) { + barBtnComics.addEventListener('click', openComicsModal); + } + + if (btnCloseComicsModal) { + btnCloseComicsModal.addEventListener('click', () => { + fabricaQuadrinhosModal.style.display = 'none'; + if (comicsVideoPlayer) comicsVideoPlayer.pause(); + }); + } + + // Fechar modal clicando fora + window.addEventListener('click', (e) => { + if (e.target === fabricaQuadrinhosModal) { + fabricaQuadrinhosModal.style.display = 'none'; + if (comicsVideoPlayer) comicsVideoPlayer.pause(); + } + }); + + // Helper para converter URL de imagem local em base64 + const getBase64Image = async (imgUrl) => { + return new Promise((resolve, reject) => { + const img = new Image(); + img.crossOrigin = 'Anonymous'; + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0); + resolve(canvas.toDataURL('image/jpeg')); + }; + img.onerror = (e) => reject(new Error('Falha ao carregar imagem para o PDF: ' + imgUrl)); + img.src = imgUrl; + }); + }; + + // Ação de geração da história e imagens + if (btnGenerateComics) { + btnGenerateComics.addEventListener('click', async () => { + const tema = comicsTemaInput.value.trim(); + if (!tema) { + alert('Por favor, digite um tema para a história.'); + return; + } + + // Reunir personagens selecionados + const personagens = []; + document.querySelectorAll('.btn-comics-char.active').forEach(b => { + personagens.push(b.textContent.trim()); + }); + + if (personagens.length === 0) { + alert('Selecione pelo menos um personagem principal.'); + return; + } + + // Definir cenário + let cenario = comicsCenarioSelect.value; + if (cenario === 'custom') { + cenario = comicsCenarioCustomInput.value.trim() || 'em um local bonito'; + } + + btnGenerateComics.disabled = true; + comicsGenerateLoader.style.display = 'flex'; + comicsResultArea.style.display = 'none'; + comicsVideoResult.style.display = 'none'; + comicsLoaderProgress.style.width = '5%'; + comicsLoaderText.textContent = 'Escrevendo o roteiro e diálogos dos quadrinhos...'; + + try { + // 1. Gerar Roteiro + const scriptResp = await fetch('/api/comics/generate-script', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + tema, + quantidade: selectedComicsQty, + baloes: selectedComicsBubbles, + proporcao: selectedComicsRatio, + personagens, + cenario + }) + }); + + if (!scriptResp.ok) { + const err = await scriptResp.json(); + throw new Error(err.error || 'Erro ao planejar roteiro'); + } + + const scriptData = await scriptResp.json(); + comicsLoaderProgress.style.width = '15%'; + + // 2. Gerar cada quadrinho sequencialmente + generatedPanelsData = []; + const totalPanels = scriptData.panels.length; + + for (let i = 0; i < totalPanels; i++) { + const panel = scriptData.panels[i]; + const progressPercent = 15 + Math.floor((i / totalPanels) * 80); + comicsLoaderProgress.style.width = `${progressPercent}%`; + comicsLoaderText.textContent = `Ilustrando quadrinho ${i + 1} de ${totalPanels}...`; + + // Injetamos a proporção e estilo de forma reforçada no prompt + const fullPrompt = `${panel.image_prompt}, high resolution children book illustration, cute Pixar style, ${selectedComicsRatio === '16:9' ? '16:9 aspect ratio' : '4:3 aspect ratio'}`; + + const frameResp = await fetch('/api/comics/generate-frame', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ prompt: fullPrompt }) + }); + + if (!frameResp.ok) { + const err = await frameResp.json(); + throw new Error(`Erro no quadrinho ${i + 1}: ${err.error}`); + } + + const frameData = await frameResp.json(); + generatedPanelsData.push({ + panel_number: panel.panel_number, + imageUrl: frameData.imageUrl, + dialogue: panel.dialogue || '' + }); + } + + // Renderizar quadrinhos na tela + comicsResultTitle.textContent = scriptData.title || 'Fábrica de Quadrinhos Camila'; + comicsGridContainer.innerHTML = ''; + + generatedPanelsData.forEach(panel => { + const panelCard = document.createElement('div'); + panelCard.className = 'comic-panel-card'; + + const imgContainer = document.createElement('div'); + imgContainer.className = `comic-panel-image-container ratio-${selectedComicsRatio.replace(':', '-')}`; + + const img = document.createElement('img'); + img.className = 'comic-panel-image'; + img.src = panel.imageUrl; + img.alt = `Quadro ${panel.panel_number}`; + imgContainer.appendChild(img); + + // Selo do número + const badge = document.createElement('div'); + badge.className = 'comic-panel-number-badge'; + badge.textContent = `Quadro ${panel.panel_number}`; + imgContainer.appendChild(badge); + + // Balão de fala absoluto por cima se habilitado e houver diálogo + if (selectedComicsBubbles && panel.dialogue) { + const bubble = document.createElement('div'); + bubble.className = 'comic-speech-bubble'; + bubble.textContent = panel.dialogue; + imgContainer.appendChild(bubble); + } + + panelCard.appendChild(imgContainer); + + // Legenda do texto abaixo + if (panel.dialogue) { + const caption = document.createElement('div'); + caption.className = 'comic-caption-text'; + caption.textContent = panel.dialogue; + panelCard.appendChild(caption); + } + + comicsGridContainer.appendChild(panelCard); + }); + + comicsGenerateLoader.style.display = 'none'; + comicsResultArea.style.display = 'flex'; + btnGenerateComics.disabled = false; + + } catch (err) { + console.error('Erro ao fabricar quadrinhos:', err); + alert('Erro ao fabricar quadrinhos: ' + err.message); + comicsGenerateLoader.style.display = 'none'; + btnGenerateComics.disabled = false; + } + }); + } + + // Exportar PDF Retrato (1 por página) + if (btnExportComicsPDFPortrait) { + btnExportComicsPDFPortrait.addEventListener('click', async () => { + if (generatedPanelsData.length === 0) return; + btnExportComicsPDFPortrait.disabled = true; + btnExportComicsPDFPortrait.textContent = '⏳ Montando PDF...'; + + try { + const { jsPDF } = window.jspdf; + const doc = new jsPDF('p', 'pt', 'a4'); + const title = comicsResultTitle.textContent || 'História em Quadrinhos'; + + for (let i = 0; i < generatedPanelsData.length; i++) { + const panel = generatedPanelsData[i]; + if (i > 0) doc.addPage(); + + // Título e Borda + doc.setDrawColor(30, 41, 59); + doc.setLineWidth(2); + doc.rect(20, 20, 555, 802); + + doc.setFont('helvetica', 'bold'); + doc.setFontSize(16); + doc.setTextColor(30, 41, 59); + doc.text(title, 297, 50, { align: 'center' }); + + // Imagem + const base64Img = await getBase64Image(panel.imageUrl); + const imgWidth = 515; + const imgHeight = selectedComicsRatio === '16:9' ? 290 : 386; + doc.addImage(base64Img, 'JPEG', 40, 80, imgWidth, imgHeight); + + // Caixa de diálogo + if (panel.dialogue) { + const textY = 100 + imgHeight; + doc.setFillColor(248, 250, 252); + doc.setDrawColor(226, 232, 240); + doc.rect(40, textY, 515, 80, 'FD'); + + doc.setFont('helvetica', 'normal'); + doc.setFontSize(12); + doc.setTextColor(15, 23, 42); + + const splitText = doc.splitTextToSize(panel.dialogue, 495); + doc.text(splitText, 50, textY + 25); + } + + // Rodapé + doc.setFont('helvetica', 'italic'); + doc.setFontSize(10); + doc.setTextColor(100, 116, 139); + doc.text(`Criado com Camila AI - Quadro ${panel.panel_number} de ${generatedPanelsData.length}`, 297, 810, { align: 'center' }); + } + + doc.save(`quadrinhos_${title.replace(/\s+/g, '_').toLowerCase()}.pdf`); + } catch (error) { + console.error('Erro ao gerar PDF Retrato:', error); + alert('Erro ao gerar PDF: ' + error.message); + } finally { + btnExportComicsPDFPortrait.disabled = false; + btnExportComicsPDFPortrait.textContent = '📄 PDF (1 por página - Retrato)'; + } + }); + } + + // Exportar PDF Paisagem (2 por página) + if (btnExportComicsPDFLandscape) { + btnExportComicsPDFLandscape.addEventListener('click', async () => { + if (generatedPanelsData.length === 0) return; + btnExportComicsPDFLandscape.disabled = true; + btnExportComicsPDFLandscape.textContent = '⏳ Montando PDF...'; + + try { + const { jsPDF } = window.jspdf; + const doc = new jsPDF('l', 'pt', 'a4'); + const title = comicsResultTitle.textContent || 'História em Quadrinhos'; + + const totalPanels = generatedPanelsData.length; + const totalPages = Math.ceil(totalPanels / 2); + + for (let page = 0; page < totalPages; page++) { + if (page > 0) doc.addPage(); + + // Desenhar moldura da página + doc.setDrawColor(30, 41, 59); + doc.setLineWidth(2); + doc.rect(20, 20, 802, 555); + + // Título + doc.setFont('helvetica', 'bold'); + doc.setFontSize(16); + doc.setTextColor(30, 41, 59); + doc.text(`${title} - Página ${page + 1} de ${totalPages}`, 421, 45, { align: 'center' }); + + // Renderizar até dois quadrinhos lado a lado + for (let col = 0; col < 2; col++) { + const index = page * 2 + col; + if (index >= totalPanels) break; + + const panel = generatedPanelsData[index]; + const startX = col === 0 ? 40 : 426; + const cardWidth = 376; + const imgHeight = selectedComicsRatio === '16:9' ? 211 : 282; + + // Imagem do quadrinho + const base64Img = await getBase64Image(panel.imageUrl); + doc.addImage(base64Img, 'JPEG', startX, 70, cardWidth, imgHeight); + + // Borda do quadrinho + doc.setDrawColor(30, 41, 59); + doc.setLineWidth(1.5); + doc.rect(startX, 70, cardWidth, imgHeight); + + // Legenda abaixo da imagem + if (panel.dialogue) { + const textY = 85 + imgHeight; + doc.setFillColor(248, 250, 252); + doc.setDrawColor(203, 213, 225); + doc.rect(startX, textY, cardWidth, 65, 'FD'); + + doc.setFont('helvetica', 'normal'); + doc.setFontSize(10); + doc.setTextColor(15, 23, 42); + + const splitText = doc.splitTextToSize(panel.dialogue, cardWidth - 20); + doc.text(splitText, startX + 10, textY + 18); + } + + // Etiqueta do número + doc.setFillColor(254, 240, 138); + doc.rect(startX + 10, 80, 50, 20, 'F'); + doc.setDrawColor(30, 41, 59); + doc.rect(startX + 10, 80, 50, 20); + doc.setFont('helvetica', 'bold'); + doc.setFontSize(9); + doc.setTextColor(30, 41, 59); + doc.text(`Quadro ${panel.panel_number}`, startX + 35, 93, { align: 'center' }); + } + } + + doc.save(`quadrinhos_${title.replace(/\s+/g, '_').toLowerCase()}_paisagem.pdf`); + } catch (error) { + console.error('Erro ao gerar PDF Paisagem:', error); + alert('Erro ao gerar PDF: ' + error.message); + } finally { + btnExportComicsPDFLandscape.disabled = false; + btnExportComicsPDFLandscape.textContent = '📄 PDF (2 por página - Paisagem)'; + } + }); + } + + // Compilar Vídeo de Apresentação (FFmpeg) + if (btnCompileComicsVideo) { + btnCompileComicsVideo.addEventListener('click', async () => { + if (generatedPanelsData.length === 0) return; + + btnCompileComicsVideo.disabled = true; + comicsVideoLoader.style.display = 'flex'; + comicsVideoResult.style.display = 'none'; + + try { + const response = await fetch('/api/comics/generate-video', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + frames: generatedPanelsData, + musicSelection: comicsVideoMusic.value, + frameDuration: comicsVideoDuration.value + }) + }); + + if (!response.ok) { + const err = await response.json(); + throw new Error(err.error || 'Erro na compilação do vídeo'); + } + + const data = await response.json(); + + comicsVideoPlayer.src = data.videoUrl; + comicsVideoDownloadBtn.href = data.videoUrl; + + comicsVideoResult.style.display = 'flex'; + comicsVideoLoader.style.display = 'none'; + btnCompileComicsVideo.disabled = false; + + } catch (err) { + console.error('Erro ao gerar vídeo dos quadrinhos:', err); + alert('Erro ao compilar o vídeo: ' + err.message); + comicsVideoLoader.style.display = 'none'; + btnCompileComicsVideo.disabled = false; + } + }); + } }; // Player de áudio personalizado global para as mídias geradas diff --git a/public/assets/audio/alegre.mp3 b/public/assets/audio/alegre.mp3 new file mode 100644 index 0000000..c427a28 Binary files /dev/null and b/public/assets/audio/alegre.mp3 differ diff --git a/public/assets/audio/aventura.mp3 b/public/assets/audio/aventura.mp3 new file mode 100644 index 0000000..bb5d9d6 Binary files /dev/null and b/public/assets/audio/aventura.mp3 differ diff --git a/public/assets/audio/calma.mp3 b/public/assets/audio/calma.mp3 new file mode 100644 index 0000000..56cd700 Binary files /dev/null and b/public/assets/audio/calma.mp3 differ diff --git a/public/index.html b/public/index.html index cc4934a..b72ab4b 100644 --- a/public/index.html +++ b/public/index.html @@ -234,6 +234,9 @@ + @@ -729,13 +732,176 @@

           
         
-
       
     
   
 
 
+  
+  
+
   
+  
   
   
   
diff --git a/public/style.css b/public/style.css
index 1f7fd8b..bf5375b 100644
--- a/public/style.css
+++ b/public/style.css
@@ -3137,4 +3137,95 @@ code {
   color: rgb(253, 224, 71);
 }
 
+/* ==========================================================================
+   FÁBRICA DE QUADRINHOS
+   ========================================================================== */
+.comic-panel-card {
+  position: relative;
+  border: 3px solid #1e293b;
+  background: var(--bg-secondary);
+  box-shadow: 4px 4px 0px #1e293b;
+  border-radius: 8px;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  transition: transform 0.2s, box-shadow 0.2s;
+}
+.comic-panel-card:hover {
+  transform: translate(-2px, -2px);
+  box-shadow: 6px 6px 0px #1e293b;
+}
+
+.comic-panel-image-container {
+  position: relative;
+  width: 100%;
+  overflow: hidden;
+  background: var(--bg-tertiary);
+  border-bottom: 2px solid #1e293b;
+}
+
+.comic-panel-image-container.ratio-16-9 {
+  aspect-ratio: 16/9;
+}
+
+.comic-panel-image-container.ratio-4-3 {
+  aspect-ratio: 4/3;
+}
+
+.comic-panel-image {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.comic-speech-bubble {
+  position: absolute;
+  top: 10px;
+  left: 10px;
+  background: white;
+  border: 2px solid #1e293b;
+  border-radius: 12px;
+  padding: 6px 10px;
+  font-size: 0.78rem;
+  font-weight: 700;
+  color: #0f172a;
+  max-width: 80%;
+  box-shadow: 2px 2px 0px rgba(30, 41, 59, 0.15);
+  z-index: 10;
+}
+
+.comic-panel-number-badge {
+  position: absolute;
+  top: 10px;
+  right: 10px;
+  background: #fef08a; /* Amarelo claro */
+  color: #1e293b;
+  font-weight: 800;
+  font-size: 0.8rem;
+  padding: 2px 6px;
+  border: 2px solid #1e293b;
+  border-radius: 6px;
+  z-index: 12;
+  box-shadow: 1px 1px 0px #1e293b;
+}
+
+.comic-caption-text {
+  padding: 10px 14px;
+  background: var(--bg-primary);
+  color: var(--text-primary);
+  font-size: 0.82rem;
+  line-height: 1.4;
+  border-top: 1px solid var(--border-light);
+  font-weight: 500;
+}
+
+.btn-comics-qty.active, .btn-comics-bubbles.active, .btn-comics-ratio.active, .btn-comics-char.active {
+  background-color: rgba(219, 39, 119, 0.15);
+  border-color: #db2777;
+  color: #f472b6;
+  font-weight: 600;
+  box-shadow: 0 0 8px rgba(219, 39, 119, 0.2);
+}
+
+
 
diff --git a/server.js b/server.js
index eb5d386..1886a1a 100644
--- a/server.js
+++ b/server.js
@@ -317,6 +317,193 @@ Retorne APENAS a letra formatada em versos com quebras de linha. Não adicione n
   }
 });
 
+// ============================================================
+// ROTAS DO ESTÚDIO: FÁBRICA DE QUADRINHOS
+// ============================================================
+
+// 1. Gerar o roteiro (script) dos quadrinhos usando MiniMax-M3
+app.post('/api/comics/generate-script', requireAuth, async (req, res) => {
+  const { tema, quantidade, baloes, proporcao, personagens, cenario } = req.body;
+
+  if (!tema) {
+    return res.status(400).json({ error: 'O tema dos quadrinhos é obrigatório.' });
+  }
+
+  try {
+    const prompt = `Crie uma história em quadrinhos pedagógica infantil com o tema: "${tema}".
+A história deve conter exatamente ${quantidade} quadrinhos.
+Baloes de falas: ${baloes ? 'Sim, a história deve ter falas/diálogos curtos para os personagens' : 'Não, a história é silenciosa, sem falas, apenas a descrição da cena'}.
+Proporção visual: ${proporcao}.
+Personagens participantes: ${JSON.stringify(personagens)}.
+Cenário/Local principal: ${cenario}.
+
+Gere um JSON perfeitamente válido contendo:
+{
+  "title": "Título da História",
+  "character_description": "Descrição visual constante dos personagens em inglês (para usar em geradores de imagens, detalhando roupas específicas, cores, características físicas de forma fixa para manter a consistência).",
+  "panels": [
+    {
+      "panel_number": 1,
+      "image_prompt": "Prompt visual detalhado em inglês para o gerador de imagens da MiniMax descrevendo a cena deste quadrinho (ex: 'Lucas and Mariana standing in a colorful sunny park, holding hands...'). Deve incluir a descrição física constante de cada personagem participante neste quadrinho. O estilo deve ser de ilustração de livro infantil de alta qualidade, 3D Pixar, traço limpo.",
+      "dialogue": "Falas do quadrinho em português (ex: 'Lucas: Olá, Mariana!') ou narração curta caso não tenha balões."
+    },
+    ...
+  ]
+}
+
+Responda APENAS o JSON válido, sem qualquer texto explicativo antes ou depois.`;
+
+    const r = await callMinimax({
+      system: "Você é um roteirista pedagógico especialista em histórias em quadrinhos infantis para educação básica. Você responde apenas com estruturas JSON válidas.",
+      messages: [{ role: 'user', content: prompt }],
+      temperature: 0.7,
+      max_tokens: 2500
+    });
+
+    let jsonText = r.text;
+    // Tenta limpar possíveis tags markdown de código
+    jsonText = jsonText.replace(/```json/gi, '').replace(/```/g, '').trim();
+
+    const scriptData = JSON.parse(jsonText);
+    res.json(scriptData);
+  } catch (error) {
+    console.error('Erro ao gerar roteiro dos quadrinhos:', error);
+    res.status(500).json({ error: 'Erro ao compor a história em quadrinhos: ' + error.message });
+  }
+});
+
+// 2. Gerar uma imagem individual para um quadrinho usando MiniMax image-01
+app.post('/api/comics/generate-frame', requireAuth, async (req, res) => {
+  const { prompt } = req.body;
+
+  if (!prompt) {
+    return res.status(400).json({ error: 'O prompt de imagem é obrigatório.' });
+  }
+
+  try {
+    const minmBase = (process.env.MINIMAX_API_BASE || 'https://api.minimax.io/v1').replace(/\/v1\/?$/, '');
+    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: prompt,
+        n: 1
+      })
+    });
+
+    if (!genResp.ok) {
+      const errText = await genResp.text();
+      throw new Error(`MiniMax image-01 respondeu erro: ${errText}`);
+    }
+
+    const genData = await genResp.json();
+    const imageUrl = genData.data?.image_urls?.[0];
+    if (!imageUrl) {
+      throw new Error('Nenhuma URL de imagem retornada pela MiniMax.');
+    }
+
+    // Baixar e salvar localmente
+    const fs = require('fs');
+    const path = require('path');
+    const mediaDir = path.join(__dirname, 'public', 'generated-media');
+    if (!fs.existsSync(mediaDir)) {
+      fs.mkdirSync(mediaDir, { recursive: true });
+    }
+    const imgFetch = await fetch(imageUrl);
+    if (!imgFetch.ok) {
+      throw new Error(`Falha ao baixar imagem: ${imgFetch.statusText}`);
+    }
+    const imgBuffer = Buffer.from(await imgFetch.arrayBuffer());
+    const fileName = `quadrinho_${Date.now()}_${Math.floor(Math.random() * 1000)}.jpg`;
+    const filePath = path.join(mediaDir, fileName);
+    fs.writeFileSync(filePath, imgBuffer);
+    
+    res.json({ imageUrl: `/generated-media/${fileName}` });
+  } catch (error) {
+    console.error('Erro ao gerar quadrinho:', error);
+    res.status(500).json({ error: error.message || 'Erro ao gerar ilustração do painel' });
+  }
+});
+
+// 3. Compilar a apresentação de vídeo usando FFmpeg a partir dos painéis de quadrinhos
+app.post('/api/comics/generate-video', requireAuth, async (req, res) => {
+  const { frames, musicSelection, frameDuration } = req.body;
+
+  if (!frames || !Array.isArray(frames) || frames.length === 0) {
+    return res.status(400).json({ error: 'Nenhum painel enviado para geração de vídeo.' });
+  }
+
+  const duration = frameDuration ? parseInt(frameDuration) : 5;
+  const musicFile = musicSelection || 'alegre';
+
+  try {
+    const fs = require('fs');
+    const path = require('path');
+    const { exec } = require('child_process');
+
+    const mediaDir = path.join(__dirname, 'public', 'generated-media');
+    if (!fs.existsSync(mediaDir)) {
+      fs.mkdirSync(mediaDir, { recursive: true });
+    }
+
+    // Mapear trilha musical
+    let bgMusicPath = path.join(__dirname, 'public', 'assets', 'audio', 'alegre.mp3');
+    if (musicFile === 'calma') {
+      bgMusicPath = path.join(__dirname, 'public', 'assets', 'audio', 'calma.mp3');
+    } else if (musicFile === 'aventura') {
+      bgMusicPath = path.join(__dirname, 'public', 'assets', 'audio', 'aventura.mp3');
+    }
+
+    // Criar arquivo de input para o FFmpeg Concat
+    const tempFileId = `${Date.now()}_${Math.floor(Math.random() * 1000)}`;
+    const concatFilePath = path.join(__dirname, `concat_${tempFileId}.txt`);
+    
+    let concatContent = '';
+    frames.forEach(frame => {
+      const localFilePath = path.join(__dirname, 'public', frame.imageUrl);
+      concatContent += `file '${localFilePath}'\nduration ${duration}\n`;
+    });
+    // O concat do FFmpeg requer repetir o último arquivo no final
+    const lastLocalFilePath = path.join(__dirname, 'public', frames[frames.length - 1].imageUrl);
+    concatContent += `file '${lastLocalFilePath}'\n`;
+
+    fs.writeFileSync(concatFilePath, concatContent);
+
+    const outputFileName = `video_quadrinhos_${tempFileId}.mp4`;
+    const outputFilePath = path.join(mediaDir, outputFileName);
+    const totalDuration = frames.length * duration;
+
+    // Comando FFmpeg: concatena as imagens, adiciona o áudio, ajusta o codec para h264/aac e encerra na duração final
+    const ffmpegCommand = `/usr/bin/ffmpeg -y -f concat -safe 0 -i "${concatFilePath}" -i "${bgMusicPath}" -c:v libx264 -pix_fmt yuv420p -c:a aac -shortest -t ${totalDuration} "${outputFilePath}"`;
+
+    console.log('[FFmpeg] Executando comando de vídeo:', ffmpegCommand);
+
+    exec(ffmpegCommand, (error, stdout, stderr) => {
+      // Remove o arquivo concat temporário
+      try {
+        fs.unlinkSync(concatFilePath);
+      } catch (e) {
+        console.error('Falha ao remover arquivo concat temporário:', e);
+      }
+
+      if (error) {
+        console.error('Erro ao processar FFmpeg:', error, stderr);
+        return res.status(500).json({ error: 'Erro ao gerar o vídeo da apresentação.' });
+      }
+
+      res.json({ videoUrl: `/generated-media/${outputFileName}` });
+    });
+
+  } catch (error) {
+    console.error('Erro ao compilar vídeo de apresentação:', error);
+    res.status(500).json({ error: 'Erro ao gerar o vídeo: ' + error.message });
+  }
+});
+
 // Rota GET para obter configurações do agente
 app.get('/api/agent-config', requireAuth, (req, res) => {
   const config = readAgentConfig();