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();