feat: implementa a Fabrica de Quadrinhos com geracao de PDF e video animado via FFmpeg
This commit is contained in:
+469
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user