feat: implementa a Fabrica de Quadrinhos com geracao de PDF e video animado via FFmpeg

This commit is contained in:
2026-06-03 12:01:03 +00:00
parent 4690672245
commit 0dbf687ec9
7 changed files with 914 additions and 1 deletions
+469
View File
@@ -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