diff --git a/public/app.js b/public/app.js index 6f82a64..ed89a4b 100644 --- a/public/app.js +++ b/public/app.js @@ -2843,13 +2843,6 @@ const initApp = () => { btnExitRecord.addEventListener('click', closeRecordModal); } - // Fechar clicando fora - if (recordModal) { - recordModal.addEventListener('click', (e) => { - if (e.target === recordModal) closeRecordModal(); - }); - } - // ============================================================ // MODAL: MINHAS OBSERVAÇÕES // ============================================================ @@ -2914,11 +2907,6 @@ const initApp = () => { obsModal.style.display = 'none'; }); } - if (obsModal) { - obsModal.addEventListener('click', (e) => { - if (e.target === obsModal) obsModal.style.display = 'none'; - }); - } // Filtros if (obsFilterMes) obsFilterMes.addEventListener('change', renderObsList); @@ -3317,11 +3305,6 @@ const initApp = () => { } if (btnCloseMinhaTurmaModal) btnCloseMinhaTurmaModal.addEventListener('click', () => minhaTurmaModal.style.display = 'none'); - if (minhaTurmaModal) { - minhaTurmaModal.addEventListener('click', (e) => { - if (e.target === minhaTurmaModal) minhaTurmaModal.style.display = 'none'; - }); - } if (btnCreateChild) btnCreateChild.addEventListener('click', clearChildForm); if (btnCancelChild) btnCancelChild.addEventListener('click', clearChildForm); @@ -3507,11 +3490,6 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.` modelosRelatorioModal.style.display = 'none'; }); } - if (modelosRelatorioModal) { - modelosRelatorioModal.addEventListener('click', (e) => { - if (e.target === modelosRelatorioModal) modelosRelatorioModal.style.display = 'none'; - }); - } // Buscar modelos do backend async function fetchTemplates() { @@ -3737,11 +3715,6 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.` emitirRelatorioModal.style.display = 'none'; }); } - if (emitirRelatorioModal) { - emitirRelatorioModal.addEventListener('click', (e) => { - if (e.target === emitirRelatorioModal) emitirRelatorioModal.style.display = 'none'; - }); - } // Buscar crianças com base nos filtros de Ano e Turma selecionados async function loadChildrenList() { @@ -4152,14 +4125,6 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.` }); } - // Fechar modal clicando fora - window.addEventListener('click', (e) => { - if (e.target === estudioMusicalModal) { - estudioMusicalModal.style.display = 'none'; - musicStudioAudioPlayer.pause(); - } - }); - // Ação de gerar música no estúdio if (btnGenerateStudioMusic) { btnGenerateStudioMusic.addEventListener('click', async () => { @@ -5069,11 +5034,7 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.` let completedCount = 0; const generateSinglePanel = async (panel) => { - let anchorPart = ''; - if (currentComicsCharDescEnglish) { - anchorPart = `CHARACTER ANCHOR: [${currentComicsCharDescEnglish}]. `; - } - const fullPrompt = `${anchorPart}${panel.image_prompt}, high resolution children book illustration, cute Pixar style, ${selectedComicsRatio === '16:9' ? '16:9 aspect ratio' : '4:3 aspect ratio'}`; + const fullPrompt = `${panel.image_prompt}, 3D Pixar Disney style, digital children book illustration, vibrant colors, masterpiece, cinematic lighting, ${selectedComicsRatio === '16:9' ? '16:9 aspect ratio' : '4:3 aspect ratio'}`; let attempts = 0; let frameData = null; @@ -5091,11 +5052,11 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.` frameData = await frameResp.json(); } else { console.warn(`[Retry Comics] Tentativa ${attempts} para quadro ${panel.panel_number} respondeu ${frameResp.status}`); - if (attempts < 3) await new Promise(r => setTimeout(r, 1200 * attempts)); + if (attempts < 3) await new Promise(r => setTimeout(r, 1500 * attempts)); } } catch (netErr) { console.warn(`[Retry Comics] Tentativa ${attempts} erro de rede no quadro ${panel.panel_number}:`, netErr.message); - if (attempts < 3) await new Promise(r => setTimeout(r, 1200 * attempts)); + if (attempts < 3) await new Promise(r => setTimeout(r, 1500 * attempts)); } } @@ -5104,6 +5065,9 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.` if (comicsLoaderProgress) comicsLoaderProgress.style.width = `${progressPercent}%`; if (comicsLoaderText) comicsLoaderText.textContent = `🎨 Ilustrando quadrinhos: ${completedCount}/${totalPanels} quadros concluídos...`; + // Pequeno respiro entre gerações para respeitar a taxa da API + await new Promise(r => setTimeout(r, 600)); + if (frameData && frameData.imageUrl) { return { panel_number: panel.panel_number, @@ -5113,7 +5077,7 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.` needsRegeneration: !!frameData.needsRegeneration }; } else { - console.warn(`[Comics Resiliencia] Quadro ${panel.panel_number} não pôde ser gerado após 3 tentativas. Preservando a história e liberando botão de regenerar.`); + console.warn(`[Comics Resiliencia] Quadro ${panel.panel_number} em espera. Preservando a história.`); return { panel_number: panel.panel_number, imageUrl: '/assets/img/placeholder_comic.png', @@ -5124,8 +5088,8 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.` } }; - // Executa com no máximo 2 quadros em paralelo para estabilidade máxima e resposta fluida - const newPanels = await mapConcurrent(scriptData.panels, 2, generateSinglePanel); + // Executa com 1 quadro por vez sequencial para estabilidade absoluta e sem estourar rate limit + const newPanels = await mapConcurrent(scriptData.panels, 1, generateSinglePanel); // Ordena por panel_number para garantir a sequência correta newPanels.sort((a, b) => a.panel_number - b.panel_number); @@ -5343,12 +5307,7 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.` } } - // Monta o prompt reforçado com a Âncora Visual Física de Personagens e Estilo 3D Pixar - let anchorPart = ''; - if (currentComicsCharDescEnglish) { - anchorPart = `CHARACTER ANCHOR: [${currentComicsCharDescEnglish}]. `; - } - const fullPrompt = `${anchorPart}${promptToUse}, high resolution children book illustration, cute Pixar style, ${selectedComicsRatio === '16:9' ? '16:9 aspect ratio' : '4:3 aspect ratio'}`; + const fullPrompt = `${promptToUse}, 3D Pixar Disney style, digital children book illustration, vibrant colors, masterpiece, cinematic lighting, ${selectedComicsRatio === '16:9' ? '16:9 aspect ratio' : '4:3 aspect ratio'}`; const resp = await fetch('/api/comics/generate-frame', { method: 'POST', @@ -5844,12 +5803,6 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.` }); } - window.addEventListener('click', (e) => { - if (e.target === videoMindModal) { - videoMindModal.style.display = 'none'; - } - }); - // Ação de analisar vídeo if (btnGenerateVideoMind) { btnGenerateVideoMind.addEventListener('click', async () => { @@ -6096,12 +6049,6 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.` }); } - window.addEventListener('click', (e) => { - if (e.target === garatujasModal) { - garatujasModal.style.display = 'none'; - } - }); - // Upload trigger if (btnUploadGaratuja) { btnUploadGaratuja.addEventListener('click', () => { @@ -6534,13 +6481,6 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.` }); } - poesiaModal.addEventListener('click', (e) => { - if (e.target === poesiaModal) { - poesiaModal.style.display = 'none'; - if (poesiaAudioPlayer) poesiaAudioPlayer.pause(); - } - }); - // Aplicar Template const applyPoesiaTemplate = (key) => { const tmpl = POESIA_TEMPLATES[key]; @@ -6867,12 +6807,6 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.` }); } - poesiaHistoryModal.addEventListener('click', (e) => { - if (e.target === poesiaHistoryModal) { - poesiaHistoryModal.style.display = 'none'; - } - }); - async function loadPoesiaHistory() { const listContainer = document.getElementById('poesiaHistoryList'); if (!listContainer) return; @@ -7038,12 +6972,6 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.` }); } - window.addEventListener('click', (e) => { - if (e.target === brincarAtivoModal) { - brincarAtivoModal.style.display = 'none'; - } - }); - // Gerar Atividade if (btnGenerateBrincarAtivo) { btnGenerateBrincarAtivo.addEventListener('click', async () => { @@ -7473,13 +7401,6 @@ window.toggleCustomAudio = (btn) => { }); } - musicaModal.addEventListener('click', (e) => { - if (e.target === musicaModal) { - musicaModal.style.display = 'none'; - if (musicaAudioPlayer) musicaAudioPlayer.pause(); - } - }); - // Aplicar Template const applyMusicaTemplate = (key) => { const tmpl = MUSICA_TEMPLATES[key]; @@ -7774,12 +7695,6 @@ window.toggleCustomAudio = (btn) => { }); } - musicaHistoryModal.addEventListener('click', (e) => { - if (e.target === musicaHistoryModal) { - musicaHistoryModal.style.display = 'none'; - } - }); - async function loadMusicaHistory() { const listContainer = document.getElementById('musicaHistoryList'); if (!listContainer) return; @@ -8053,7 +7968,6 @@ window.toggleCustomAudio = (btn) => { } if (btnCloseHistoriaModal) { - historiaModal.addEventListener('click', (e) => { if (e.target === historiaModal) historiaModal.style.display = 'none'; }); btnCloseHistoriaModal.addEventListener('click', () => historiaModal.style.display = 'none'); } @@ -8065,7 +7979,6 @@ window.toggleCustomAudio = (btn) => { }); } if (btnCloseHistoriaHistory) btnCloseHistoriaHistory.addEventListener('click', () => historiaHistoryModal.style.display = 'none'); - historiaHistoryModal.addEventListener('click', (e) => { if (e.target === historiaHistoryModal) historiaHistoryModal.style.display = 'none'; }); // Alternância de Abas function switchHistoriaTab(tab) { @@ -8612,9 +8525,6 @@ document.addEventListener('DOMContentLoaded', () => { btnCloseMindLabModal.addEventListener('click', () => { mindLabModal.style.display = 'none'; }); - mindLabModal.addEventListener('click', (e) => { - if (e.target === mindLabModal) mindLabModal.style.display = 'none'; - }); } if (btnGenerateMindLab) { @@ -11024,7 +10934,6 @@ document.addEventListener('DOMContentLoaded', () => { let currentTimelineAlunoId = null; if (btnCloseAlunoTimeline) btnCloseAlunoTimeline.addEventListener('click', () => alunoTimelineModal.style.display = 'none'); - if (alunoTimelineModal) alunoTimelineModal.addEventListener('click', (e) => { if (e.target === alunoTimelineModal) alunoTimelineModal.style.display = 'none'; }); window.viewChildTimeline = async (alunoId) => { currentTimelineAlunoId = alunoId; @@ -11196,7 +11105,6 @@ document.addEventListener('DOMContentLoaded', () => { const btnProcessCsvImport = document.getElementById('btnProcessCsvImport'); if (btnCloseCsvImport) btnCloseCsvImport.addEventListener('click', () => csvImportModal.style.display = 'none'); - if (csvImportModal) csvImportModal.addEventListener('click', (e) => { if (e.target === csvImportModal) csvImportModal.style.display = 'none'; }); // Botão abrir CSV import (se presente na tela de turmas ou sidebar) window.openCsvImportModal = () => { @@ -11299,7 +11207,6 @@ document.addEventListener('DOMContentLoaded', () => { }); } if (btnCloseCriseModal) btnCloseCriseModal.addEventListener('click', () => criseModal.style.display = 'none'); - if (criseModal) criseModal.addEventListener('click', (e) => { if (e.target === criseModal) criseModal.style.display = 'none'; }); if (btnOpenCrisesHistory) { btnOpenCrisesHistory.addEventListener('click', async () => { @@ -11437,7 +11344,6 @@ document.addEventListener('DOMContentLoaded', () => { }); } if (btnCloseAtaModal) btnCloseAtaModal.addEventListener('click', () => ataModal.style.display = 'none'); - if (ataModal) ataModal.addEventListener('click', (e) => { if (e.target === ataModal) ataModal.style.display = 'none'; }); if (btnGerarAta) { btnGerarAta.addEventListener('click', async () => { @@ -11617,7 +11523,6 @@ document.addEventListener('DOMContentLoaded', () => { }); } if (btnCloseStickerModal) btnCloseStickerModal.addEventListener('click', () => stickerModal.style.display = 'none'); - if (stickerModal) stickerModal.addEventListener('click', (e) => { if (e.target === stickerModal) stickerModal.style.display = 'none'; }); window.deleteStickerRecord = async (id) => { if (confirm('Deseja excluir este sticker?')) { diff --git a/server.js b/server.js index 5847968..dcddf93 100644 --- a/server.js +++ b/server.js @@ -513,57 +513,48 @@ app.post('/api/comics/generate-script', requireAuth, async (req, res) => { try { let prompt = ''; - if (existingPanels && Array.isArray(existingPanels) && existingPanels.length > 0) { - const startPanelNum = continuationType === 'extend' ? existingPanels.length + 1 : 1; - - prompt = `Você está continuando ou se baseando em um projeto de história em quadrinhos existente. -DADOS DO PROJETO EXISTENTE: -- Descrição visual física e de estilo constante dos personagens (em inglês): "${character_description || ''}" -- Painéis já existentes (roteiro anterior): -${existingPanels.map(p => ` * Quadrinho ${p.panel_number || p.panelNumber}: [Visual] ${p.image_prompt || p.imagePrompt || 'N/A'} | [Fala/Legenda] ${p.dialogue}`).join('\n')} + const isContinuation = existingPanels && Array.isArray(existingPanels) && existingPanels.length > 0 && continuationType && continuationType !== 'new'; + const startPanelNum = (existingPanels && Array.isArray(existingPanels)) ? existingPanels.length + 1 : 1; -INSTRUÇÕES DE GERAÇÃO: -Tipo de continuidade: ${continuationType === 'extend' ? 'Estender a mesma história (continuação cronológica direta, criando novos painéis sequenciais)' : 'Criar uma nova história separada (spin-off com os mesmos personagens, recomeçando do painel 1)'}. -Tema/Objetivo da nova fase: "${tema}". -Gere exatamente ${quantidade} novos quadrinhos. -Baloes de falas: ${baloes ? 'Sim, cada quadrinho deve conter um diálogo/fala extremamente conciso e curto, contendo no máximo 10 palavras no total.' : 'Não, a história é silenciosa, sem falas, apenas a descrição da cena'}. + if (isContinuation) { + prompt = `Continue a história em quadrinhos pedagógica infantil com o tema: "${tema}". +A história deve conter mais ${quantidade} novos quadrinhos dando continuidade cronológica aos anteriores. +Baloes de falas: ${baloes ? 'Sim, cada quadrinho deve conter um diálogo/fala conciso e expressivo, contendo no máximo 12 palavras no total.' : 'Não, a história é silenciosa, sem falas, apenas a ação dos personagens'}. Proporção visual: ${proporcao}. Cenário/Local principal da nova fase: ${cenario}. Gere um JSON perfeitamente válido contendo: { "title": "${continuationType === 'extend' ? 'Manter o título original ou propor um subtítulo condizente' : 'Novo Título da História'}", - "character_description": "${character_description || 'Descrição física rigorosamente constante e fixa dos personagens em inglês (roupas fixas, cabelos, cores)'}", + "character_description": "${character_description || 'Descrição física fixa dos personagens em inglês (roupas fixas, cabelos, cores)'}", "panels": [ { "panel_number": ${startPanelNum}, - "image_prompt": "Prompt visual minucioso em inglês para o gerador de imagens. REGRA RÍGIDA DE CONSISTÊNCIA: Substitua nomes de personagens por sua descrição física EXATA e FIXA em inglês (do character_description) em TODOS os quadrinhos onde aparecerem (mesmas roupas, mesma cor de cabelo, mesmas características). Descreva a ação específica do quadrinho mantendo o mesmo estilo visual 3D Pixar.", - "dialogue": "Falas do quadrinho em português (ex: 'Lucas: Veja aquilo!') com NO MÁXIMO 10 palavras, ou narração curta caso não tenha balões." - }, - ... + "image_prompt": "Cena cinematográfica completa em inglês em estilo 3D Pixar/Disney: descreva a ação dos personagens com suas roupas e traços físicos exatos inseridos organicamente no cenário (${cenario}), com iluminação mágica, cores vibrantes e atmosfera infantil encantadora.", + "dialogue": "Falas do quadrinho em português (ex: 'Lucas: Veja aquilo!') com NO MÁXIMO 12 palavras, ou narração curta caso não tenha balões." + } ] } Responda APENAS o JSON válido, sem qualquer texto explicativo antes ou depois.`; } else { - prompt = `Crie uma história em quadrinhos pedagógica infantil com o tema: "${tema}". + prompt = `Crie uma história em quadrinhos pedagógica infantil rica e envolvente com o tema: "${tema}". A história deve conter exatamente ${quantidade} quadrinhos. -Baloes de falas: ${baloes ? 'Sim, cada quadrinho deve conter um diálogo/fala extremamente conciso e curto, contendo no máximo 10 palavras no total.' : 'Não, a história é silenciosa, sem falas, apenas a descrição da cena'}. +Baloes de falas: ${baloes ? 'Sim, cada quadrinho deve conter um diálogo/fala conciso e expressivo, contendo no máximo 12 palavras no total.' : '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 física rigorosamente detalhada e fixa dos personagens em inglês (especifique roupas fixas, cores de camiseta/vestido, tipo/cor de cabelo, tom de pele e características físicas fixas para NUNCA mudarem entre os quadrinhos).", + "title": "Título Criativo da História", + "character_description": "Descrição física detalhada e fixa dos personagens em inglês (ex: Lucas: a cheerful 6-year-old boy with short brown hair wearing a bright red t-shirt and blue shorts. Mariana: a 5-year-old girl with blonde pigtails in a yellow dress).", "panels": [ { "panel_number": 1, - "image_prompt": "Prompt visual minucioso em inglês para o gerador de imagens da MiniMax. REGRA RÍGIDA DE CONSISTÊNCIA: Substitua TODOS os nomes de personagens por sua descrição física EXATA e FIXA em inglês (do character_description) em TODOS os quadrinhos em que aparecerem (mesmas roupas fixas, cabelos e características de forma idêntica em 100% das imagens). Descreva a cena específica mantendo a arte 3D Pixar infantil de traço limpo.", - "dialogue": "Falas do quadrinho em português (ex: 'Lucas: Veja aquilo!') com NO MÁXIMO 10 palavras, ou narração curta caso não tenha balões." - }, - ... + "image_prompt": "Cena cinematográfica completa em inglês em estilo 3D Pixar/Disney: descreva a ação dos personagens com suas roupas e traços físicos exatos inseridos vividamente DENTRO do cenário principal (${cenario}), com cores vibrantes, iluminação mágica e atmosfera infantil encantadora.", + "dialogue": "Falas do quadrinho em português (ex: 'Lucas: Vamos explorar a floresta!') com NO MÁXIMO 12 palavras, ou narração curta caso não tenha balões." + } ] } @@ -571,10 +562,10 @@ 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.", + system: "Você é um renomado diretor de arte e roteirista de histórias em quadrinhos infantis 3D estilo Pixar Disney. Você sempre descreve cenas ricas com personagens e cenários integrados naturalmente em inglês.", messages: [{ role: 'user', content: prompt }], - temperature: 0.3, - max_tokens: 3000 + temperature: 0.4, + max_tokens: 3500 }); let jsonText = r.text; @@ -652,9 +643,6 @@ app.post('/api/comics/generate-frame', requireAuth, async (req, res) => { fs.mkdirSync(mediaDir, { recursive: true }); } - const fileName = `quadrinho_${Date.now()}_${Math.floor(Math.random() * 1000)}.jpg`; - const filePath = path.join(mediaDir, fileName); - let imageBuffer = null; let providerUsed = 'minimax'; @@ -758,6 +746,10 @@ app.post('/api/comics/generate-frame', requireAuth, async (req, res) => { providerUsed = 'fallback-placeholder'; } + const fileExt = (providerUsed === 'fallback-placeholder') ? 'svg' : 'jpg'; + const fileName = `quadrinho_${Date.now()}_${Math.floor(Math.random() * 1000)}.${fileExt}`; + const filePath = path.join(mediaDir, fileName); + try { fs.writeFileSync(filePath, imageBuffer);