Corrige geracao de quadrinhos, adiciona fallback inteligente de imagens e resiliencia de conexao com banco de dados

This commit is contained in:
2026-08-31 13:53:33 +00:00
parent a8b763bb50
commit a08c741a4f
3 changed files with 235 additions and 106 deletions
+140 -57
View File
@@ -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.' });
}
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 fileName = `quadrinho_${Date.now()}_${Math.floor(Math.random() * 1000)}.jpg`;
const filePath = path.join(mediaDir, fileName);
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 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
})
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);
});
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' });
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,11 +824,19 @@ 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) {
await client.query('ROLLBACK');
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 {
client.release();
if (client) {
client.release();
}
}
});