Corrige geracao de quadrinhos, adiciona fallback inteligente de imagens e resiliencia de conexao com banco de dados
This commit is contained in:
+58
-45
@@ -4562,8 +4562,12 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.`
|
||||
if (!comicsProjectSelect) return;
|
||||
try {
|
||||
const resp = await fetch('/api/comics/projects');
|
||||
if (!resp.ok) throw new Error('Falha ao listar projetos');
|
||||
if (!resp.ok) {
|
||||
console.warn('Projetos de quadrinhos temporariamente indisponíveis:', resp.status);
|
||||
return;
|
||||
}
|
||||
const projects = await resp.json();
|
||||
if (!Array.isArray(projects)) return;
|
||||
|
||||
comicsProjectSelect.innerHTML = '<option value="">-- Carregar projeto salvo... --</option>';
|
||||
projects.forEach(proj => {
|
||||
@@ -4577,7 +4581,7 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.`
|
||||
comicsProjectSelect.value = currentComicsProjectId;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Erro ao listar projetos:', err);
|
||||
console.warn('Aviso ao listar projetos:', err.message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4588,7 +4592,10 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.`
|
||||
}
|
||||
try {
|
||||
const resp = await fetch(`/api/comics/projects/${id}`);
|
||||
if (!resp.ok) throw new Error('Falha ao carregar projeto');
|
||||
if (!resp.ok) {
|
||||
const errMsg = await safeExtractError(resp, 'Falha ao carregar projeto');
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
const { project, panels } = await resp.json();
|
||||
|
||||
currentComicsProjectId = project.id;
|
||||
@@ -4692,19 +4699,13 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.`
|
||||
|
||||
const caption = document.createElement('div');
|
||||
caption.className = 'comic-caption-text';
|
||||
caption.style.cssText = 'padding: 10px 14px; background: var(--bg-primary); border-top: 1px solid var(--border-light);';
|
||||
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.className = 'comic-caption-input';
|
||||
textarea.value = panel.dialogue || '';
|
||||
textarea.placeholder = 'Digite a fala ou legenda do quadrinho...';
|
||||
textarea.style.cssText = 'width: 100%; border: 1px solid var(--border-light); background: var(--bg-secondary); color: var(--text-primary); border-radius: 6px; padding: 8px; font-family: inherit; font-size: 0.85rem; resize: vertical; box-sizing: border-box; min-height: 50px;';
|
||||
|
||||
textarea.addEventListener('input', (e) => {
|
||||
panel.dialogue = e.target.value;
|
||||
if (generatedPanelsData[index]) {
|
||||
generatedPanelsData[index].dialogue = e.target.value;
|
||||
}
|
||||
});
|
||||
|
||||
caption.appendChild(textarea);
|
||||
@@ -4714,8 +4715,6 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.`
|
||||
});
|
||||
|
||||
comicsResultArea.style.display = 'flex';
|
||||
} else {
|
||||
comicsResultArea.style.display = 'none';
|
||||
}
|
||||
|
||||
comicsVideoResult.style.display = 'none';
|
||||
@@ -4729,24 +4728,19 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.`
|
||||
};
|
||||
|
||||
const saveComicsProjectFlow = async (forceNew = false) => {
|
||||
const titulo = comicsProjectTitleInput ? comicsProjectTitleInput.value.trim() : '';
|
||||
let titulo = comicsProjectTitleInput ? comicsProjectTitleInput.value.trim() : '';
|
||||
if (!titulo) {
|
||||
await showCustomAlert('Campo Obrigatório', 'Por favor, digite um título para salvar seu projeto.');
|
||||
return;
|
||||
titulo = comicsResultTitle ? comicsResultTitle.textContent.trim() : 'Minha História em Quadrinhos';
|
||||
}
|
||||
|
||||
const characters = [];
|
||||
document.querySelectorAll('.comic-char-row').forEach(row => {
|
||||
const typeInput = row.querySelector('.char-type-input');
|
||||
const nameInput = row.querySelector('.char-name-input');
|
||||
if (typeInput && nameInput) {
|
||||
const type = typeInput.value.trim();
|
||||
const name = nameInput.value.trim();
|
||||
const isAnimal = row.querySelector('span').textContent === '🐾';
|
||||
if (type || name) {
|
||||
const type = row.querySelector('.char-type-input').value.trim();
|
||||
const name = row.querySelector('.char-name-input').value.trim();
|
||||
const isAnimal = row.querySelector('.comic-char-animal-toggle').classList.contains('active');
|
||||
if (type && name) {
|
||||
characters.push({ type, name, isAnimal });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let cenario = comicsCenarioSelect ? comicsCenarioSelect.value : 'no parque de diversões colorido';
|
||||
@@ -4780,8 +4774,8 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.`
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json();
|
||||
throw new Error(err.error || 'Erro ao salvar projeto');
|
||||
const errMsg = await safeExtractError(resp, 'Erro ao salvar projeto');
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
|
||||
const data = await resp.json();
|
||||
@@ -4814,7 +4808,10 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.`
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!resp.ok) throw new Error('Falha ao excluir projeto');
|
||||
if (!resp.ok) {
|
||||
const errMsg = await safeExtractError(resp, 'Falha ao excluir projeto');
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
|
||||
await showCustomAlert('Excluído', 'Projeto excluído com sucesso!');
|
||||
startNewComicsProject();
|
||||
@@ -4900,6 +4897,21 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.`
|
||||
});
|
||||
};
|
||||
|
||||
// Helper para execução concorrente controlada
|
||||
const mapConcurrent = async (items, limit, fn) => {
|
||||
const results = new Array(items.length);
|
||||
let currentIdx = 0;
|
||||
const worker = async () => {
|
||||
while (currentIdx < items.length) {
|
||||
const i = currentIdx++;
|
||||
results[i] = await fn(items[i], i);
|
||||
}
|
||||
};
|
||||
const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
|
||||
await Promise.all(workers);
|
||||
return results;
|
||||
};
|
||||
|
||||
// Ação de geração da história e imagens
|
||||
if (btnGenerateComics) {
|
||||
btnGenerateComics.addEventListener('click', async () => {
|
||||
@@ -4968,44 +4980,48 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.`
|
||||
});
|
||||
|
||||
if (!scriptResp.ok) {
|
||||
const err = await scriptResp.json();
|
||||
throw new Error(err.error || 'Erro ao planejar roteiro');
|
||||
const errMsg = await safeExtractError(scriptResp, 'Erro ao planejar roteiro');
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
|
||||
const scriptData = await scriptResp.json();
|
||||
comicsLoaderProgress.style.width = '15%';
|
||||
comicsLoaderProgress.style.width = '20%';
|
||||
|
||||
if (scriptData.character_description) {
|
||||
currentComicsCharDescEnglish = scriptData.character_description;
|
||||
}
|
||||
|
||||
// 2. Gerar quadrinhos em PARALELO com tracking de progresso em tempo real
|
||||
// 2. Gerar quadrinhos com concorrência balanceada (2 por vez) e tracking em tempo real
|
||||
if (genMode !== 'extend') {
|
||||
generatedPanelsData = [];
|
||||
}
|
||||
const totalPanels = scriptData.panels.length;
|
||||
let completedCount = 0;
|
||||
|
||||
const panelPromises = scriptData.panels.map(async (panel, idx) => {
|
||||
// Injetamos a proporção e estilo de forma reforçada no prompt
|
||||
const generateSinglePanel = async (panel) => {
|
||||
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', {
|
||||
let frameResp;
|
||||
try {
|
||||
frameResp = await fetch('/api/comics/generate-frame', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ prompt: fullPrompt })
|
||||
body: JSON.stringify({ prompt: fullPrompt, proporcao: selectedComicsRatio })
|
||||
});
|
||||
} catch (netErr) {
|
||||
throw new Error(`Falha de conexão no quadro ${panel.panel_number}: ${netErr.message}`);
|
||||
}
|
||||
|
||||
if (!frameResp.ok) {
|
||||
const err = await frameResp.json();
|
||||
throw new Error(`Erro no quadrinho ${panel.panel_number}: ${err.error}`);
|
||||
const errMsg = await safeExtractError(frameResp, 'Erro ao ilustrar quadro');
|
||||
throw new Error(`Erro no quadrinho ${panel.panel_number}: ${errMsg}`);
|
||||
}
|
||||
|
||||
const frameData = await frameResp.json();
|
||||
completedCount++;
|
||||
const progressPercent = 15 + Math.floor((completedCount / totalPanels) * 80);
|
||||
const progressPercent = 20 + Math.floor((completedCount / totalPanels) * 75);
|
||||
if (comicsLoaderProgress) comicsLoaderProgress.style.width = `${progressPercent}%`;
|
||||
if (comicsLoaderText) comicsLoaderText.textContent = `⚡ Ilustrando em paralelo: ${completedCount}/${totalPanels} quadros concluídos...`;
|
||||
if (comicsLoaderText) comicsLoaderText.textContent = `🎨 Ilustrando quadrinhos: ${completedCount}/${totalPanels} quadros concluídos...`;
|
||||
|
||||
return {
|
||||
panel_number: panel.panel_number,
|
||||
@@ -5013,9 +5029,10 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.`
|
||||
dialogue: panel.dialogue || '',
|
||||
image_prompt: panel.image_prompt || ''
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const newPanels = await Promise.all(panelPromises);
|
||||
// Executa com no máximo 2 quadros em paralelo para estabilidade máxima e resposta fluida
|
||||
const newPanels = await mapConcurrent(scriptData.panels, 2, generateSinglePanel);
|
||||
|
||||
// Ordena por panel_number para garantir a sequência correta
|
||||
newPanels.sort((a, b) => a.panel_number - b.panel_number);
|
||||
@@ -5028,10 +5045,6 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.`
|
||||
|
||||
// Atualizar cabeçalho e metadados
|
||||
comicsResultTitle.textContent = scriptData.title || comicsResultTitle.textContent || 'Fábrica de Quadrinhos Pedagog';
|
||||
const metaPanels = document.getElementById('comicsMetaPanels');
|
||||
if (metaPanels) metaPanels.textContent = `🖼️ ${generatedPanelsData.length} Quadros`;
|
||||
const metaRatio = document.getElementById('comicsMetaRatio');
|
||||
if (metaRatio) metaRatio.textContent = `📐 ${selectedComicsRatio} ${selectedComicsRatio === '16:9' ? 'HD' : 'Retrô'}`;
|
||||
|
||||
// Renderizar Storyboard Interativo
|
||||
renderComicsStoryboard();
|
||||
@@ -5204,8 +5217,8 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.`
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json();
|
||||
throw new Error(err.error || 'Erro na regeneração');
|
||||
const errMsg = await safeExtractError(resp, 'Erro na regeneração');
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
|
||||
const data = await resp.json();
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
require('dotenv').config();
|
||||
|
||||
async function testMiniMaxImage() {
|
||||
const minmBase = (process.env.MINIMAX_API_BASE || 'https://api.minimax.io/v1').replace(/\/v1\/?$/, '');
|
||||
const apiKey = process.env.MINIMAX_API_KEY;
|
||||
|
||||
console.log('Testando MiniMax Image Generation...');
|
||||
console.log('Base URL:', minmBase);
|
||||
console.log('Chave presente:', !!apiKey);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${minmBase}/v1/image_generation`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'image-01',
|
||||
prompt: 'A cute little cartoon boy in a bright colorful playground, 3D Pixar style, child book illustration, high resolution, 16:9 aspect ratio',
|
||||
n: 1
|
||||
})
|
||||
});
|
||||
|
||||
console.log('Status HTTP:', res.status, res.statusText);
|
||||
const text = await res.text();
|
||||
console.log('Resposta bruta:', text);
|
||||
} catch (err) {
|
||||
console.error('Erro na requisição:', err);
|
||||
}
|
||||
}
|
||||
|
||||
testMiniMaxImage();
|
||||
@@ -76,10 +76,26 @@ const requireAuth = (req, res, next) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Handlers globais para evitar crash do processo Node.js por falhas transitórias de conexão
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('[CRITICAL] Exceção não capturada no processo (evitado crash):', err);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('[CRITICAL] Rejeição de Promise não tratada (evitado crash):', reason);
|
||||
});
|
||||
|
||||
const fs = require('fs');
|
||||
const { Pool } = require('pg');
|
||||
const dbPool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
connectionTimeoutMillis: 10000,
|
||||
idleTimeoutMillis: 30000,
|
||||
max: 25
|
||||
});
|
||||
|
||||
dbPool.on('error', (err) => {
|
||||
console.error('[Database Pool Error] Erro inesperado em cliente de banco (não fatal):', err.message);
|
||||
});
|
||||
|
||||
async function backupMediaFile(filePath, buffer) {
|
||||
@@ -94,15 +110,19 @@ async function backupMediaFile(filePath, buffer) {
|
||||
else if (ext === '.mp4') contentType = 'video/mp4';
|
||||
else if (ext === '.wav') contentType = 'audio/wav';
|
||||
|
||||
await dbPool.query(
|
||||
// Timeout para não travar em caso de lentidão temporária do banco
|
||||
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout no backup de mídia')), 5000));
|
||||
const queryPromise = dbPool.query(
|
||||
`INSERT INTO escola.arquivos (filename, content_type, dados)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (filename) DO UPDATE SET dados = $3, content_type = $2`,
|
||||
[filename, contentType, buffer]
|
||||
);
|
||||
|
||||
await Promise.race([queryPromise, timeoutPromise]);
|
||||
console.log(`[Media Backup] Backup do arquivo ${filename} salvo no banco escola.arquivos.`);
|
||||
} catch (err) {
|
||||
console.error(`[Media Backup] Erro ao fazer backup do arquivo ${filePath}:`, err);
|
||||
console.warn(`[Media Backup] Aviso ao fazer backup do arquivo ${filePath} no banco:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -502,62 +522,116 @@ Responda APENAS o JSON válido, sem qualquer texto explicativo antes ou depois.`
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Gerar uma imagem individual para um quadrinho usando MiniMax image-01
|
||||
// 2. Gerar uma imagem individual para um quadrinho usando MiniMax image-01 com Fallback Automático
|
||||
app.post('/api/comics/generate-frame', requireAuth, async (req, res) => {
|
||||
const { prompt } = req.body;
|
||||
const { prompt, proporcao } = 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) {
|
||||
const msg = genData.base_resp?.status_msg || 'desconhecido';
|
||||
throw new Error(`Nenhuma URL de imagem. Status MiniMax: ${msg}`);
|
||||
}
|
||||
|
||||
// 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);
|
||||
await backupMediaFile(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' });
|
||||
let imageBuffer = null;
|
||||
let providerUsed = 'minimax';
|
||||
|
||||
// Tentativa 1: MiniMax image-01 com timeout de 38s
|
||||
try {
|
||||
const minmBase = (process.env.MINIMAX_API_BASE || 'https://api.minimax.io/v1').replace(/\/v1\/?$/, '');
|
||||
const apiKey = process.env.MINIMAX_API_KEY;
|
||||
|
||||
if (apiKey) {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 38000);
|
||||
|
||||
const genResp = await fetch(`${minmBase}/v1/image_generation`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'image-01',
|
||||
prompt: prompt,
|
||||
n: 1
|
||||
}),
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (genResp.ok) {
|
||||
const genData = await genResp.json();
|
||||
const imageUrl = genData.data?.image_urls?.[0];
|
||||
if (imageUrl) {
|
||||
const imgFetch = await fetch(imageUrl);
|
||||
if (imgFetch.ok) {
|
||||
imageBuffer = Buffer.from(await imgFetch.arrayBuffer());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const errText = await genResp.text();
|
||||
console.warn('[Comics Image] MiniMax retornou erro, acionando fallback inteligente:', errText);
|
||||
}
|
||||
}
|
||||
} catch (minimaxErr) {
|
||||
console.warn('[Comics Image] Falha/Timeout no MiniMax image-01 (' + minimaxErr.message + '), acionando gerador fallback...');
|
||||
}
|
||||
|
||||
// Tentativa 2 (Fallback): Pollinations FLUX de alta resolução estilizado 3D Pixar
|
||||
if (!imageBuffer) {
|
||||
providerUsed = 'pollinations-flux';
|
||||
try {
|
||||
const is169 = proporcao === '16:9' || prompt.includes('16:9');
|
||||
const width = is169 ? 1280 : 1024;
|
||||
const height = is169 ? 720 : 768;
|
||||
const enhancedPrompt = encodeURIComponent(`${prompt}, 3d pixar disney style, digital children book illustration, vibrant colors, masterpiece, cinematic lighting`);
|
||||
const fallbackUrl = `https://image.pollinations.ai/prompt/${enhancedPrompt}?width=${width}&height=${height}&nologo=true&seed=${Math.floor(Math.random() * 100000)}`;
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
||||
|
||||
const fbResp = await fetch(fallbackUrl, { signal: controller.signal });
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (fbResp.ok) {
|
||||
imageBuffer = Buffer.from(await fbResp.arrayBuffer());
|
||||
console.log('[Comics Image] Imagem gerada com sucesso via Fallback Inteligente (Pollinations Flux).');
|
||||
} else {
|
||||
throw new Error(`Fallback HTTP ${fbResp.status}`);
|
||||
}
|
||||
} catch (fbErr) {
|
||||
console.error('[Comics Image] Erro também no provedor Fallback:', fbErr.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (!imageBuffer) {
|
||||
return res.status(500).json({ error: 'Não foi possível gerar a ilustração do quadro no momento. Por favor, tente novamente.' });
|
||||
}
|
||||
|
||||
try {
|
||||
fs.writeFileSync(filePath, imageBuffer);
|
||||
|
||||
// Backup assíncrono não bloqueante
|
||||
backupMediaFile(filePath, imageBuffer).catch(err => {
|
||||
console.warn('[Comics Backup] Falha em background no backup da imagem:', err.message);
|
||||
});
|
||||
|
||||
res.json({
|
||||
imageUrl: `/generated-media/${fileName}`,
|
||||
provider: providerUsed
|
||||
});
|
||||
} catch (saveErr) {
|
||||
console.error('Erro ao salvar imagem localmente:', saveErr);
|
||||
res.status(500).json({ error: 'Erro ao persistir imagem gerada: ' + saveErr.message });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -627,7 +701,7 @@ app.post('/api/comics/generate-video', requireAuth, async (req, res) => {
|
||||
return res.status(500).json({ error: 'Erro ao gerar o vídeo da apresentação. Como o servidor foi atualizado e reiniciado recentemente, por favor crie/gere a história em quadrinhos novamente para recriar as imagens temporárias antes de exportar o vídeo.' });
|
||||
}
|
||||
|
||||
// Backup do vídeo gerado no banco de dados
|
||||
// Backup do vídeo gerado no banco de dados em background
|
||||
(async () => {
|
||||
try {
|
||||
const videoBuffer = fs.readFileSync(outputFilePath);
|
||||
@@ -656,10 +730,10 @@ app.get('/api/comics/projects', requireAuth, async (req, res) => {
|
||||
'SELECT id, titulo, tema, cenario, proporcao, character_description_global, character_description_english, created_at, updated_at FROM escola.projetos_comics WHERE usuario_id = $1 ORDER BY updated_at DESC;',
|
||||
[usuarioId]
|
||||
);
|
||||
res.json(result.rows);
|
||||
res.json(result.rows || []);
|
||||
} catch (error) {
|
||||
console.error('Erro ao listar projetos de quadrinhos:', error);
|
||||
res.status(500).json({ error: 'Erro ao listar projetos de quadrinhos.' });
|
||||
console.error('Erro ao listar projetos de quadrinhos:', error.message);
|
||||
res.json([]);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -683,7 +757,7 @@ app.get('/api/comics/projects/:id', requireAuth, async (req, res) => {
|
||||
|
||||
res.json({
|
||||
project: projectRes.rows[0],
|
||||
panels: panelsRes.rows
|
||||
panels: panelsRes.rows || []
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao obter projeto de quadrinhos:', error);
|
||||
@@ -700,8 +774,9 @@ app.post('/api/comics/projects', requireAuth, async (req, res) => {
|
||||
return res.status(400).json({ error: 'O título do projeto é obrigatório.' });
|
||||
}
|
||||
|
||||
const client = await dbPool.connect();
|
||||
let client = null;
|
||||
try {
|
||||
client = await dbPool.connect();
|
||||
await client.query('BEGIN');
|
||||
|
||||
let projetoId = id;
|
||||
@@ -749,12 +824,20 @@ app.post('/api/comics/projects', requireAuth, async (req, res) => {
|
||||
await client.query('COMMIT');
|
||||
res.json({ success: true, id: projetoId, message: 'Projeto salvo com sucesso!' });
|
||||
} catch (error) {
|
||||
if (client) {
|
||||
try {
|
||||
await client.query('ROLLBACK');
|
||||
} catch (rbErr) {
|
||||
console.warn('Erro ao executar ROLLBACK:', rbErr.message);
|
||||
}
|
||||
}
|
||||
console.error('Erro ao salvar projeto de quadrinhos:', error);
|
||||
res.status(500).json({ error: 'Erro ao salvar projeto: ' + error.message });
|
||||
} finally {
|
||||
if (client) {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 4. Excluir projeto
|
||||
|
||||
Reference in New Issue
Block a user