fix: resolve erro PayloadTooLarge ao salvar midias localmente e integra veo-3.1-lite para video
This commit is contained in:
+9
-2
@@ -1273,8 +1273,15 @@ const initApp = () => {
|
||||
let assistantContent = '';
|
||||
|
||||
try {
|
||||
// Filtrar mensagens para enviar apenas o histórico necessário ao OpenRouter
|
||||
const messagesPayload = chat.messages.map(m => ({ role: m.role, content: m.content }));
|
||||
// Filtrar mensagens para enviar apenas o histórico necessário ao OpenRouter, removendo dados base64 pesados de mídias
|
||||
const messagesPayload = chat.messages.map(m => {
|
||||
let cleanContent = m.content || '';
|
||||
if (cleanContent.includes('data:image/') || cleanContent.includes('data:audio/') || cleanContent.includes('data:video/')) {
|
||||
const typeLabel = cleanContent.includes('custom-audio-player') ? 'Áudio/Música' : (cleanContent.includes('media-video-container') ? 'Vídeo' : 'Imagem');
|
||||
cleanContent = `[Mídia gerada anteriormente: ${typeLabel}]`;
|
||||
}
|
||||
return { role: m.role, content: cleanContent };
|
||||
});
|
||||
|
||||
// Fazer a chamada HTTP usando stream
|
||||
const response = await fetch('/api/chat', {
|
||||
|
||||
@@ -7,7 +7,8 @@ const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const ACCESS_PASSWORD = process.env.ACCESS_PASSWORD || 'puglindo26';
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.json({ limit: '50mb' }));
|
||||
app.use(express.urlencoded({ limit: '50mb', extended: true }));
|
||||
app.use(cookieParser(process.env.SESSION_SECRET || 'camila-secret'));
|
||||
|
||||
// Middleware para verificar autenticação
|
||||
@@ -441,12 +442,32 @@ app.post('/api/chat', requireAuth, async (req, res) => {
|
||||
|
||||
const imgData = await response.json();
|
||||
const imageObj = imgData.choices?.[0]?.message?.images?.[0];
|
||||
const imageUrl = imageObj?.image_url?.url;
|
||||
let imageUrl = imageObj?.image_url?.url;
|
||||
|
||||
if (!imageUrl) {
|
||||
throw new Error('Nenhuma imagem foi retornada pelo OpenRouter');
|
||||
}
|
||||
|
||||
// Se for base64, salvar localmente no servidor
|
||||
if (imageUrl.startsWith('data:')) {
|
||||
const fs = require('fs');
|
||||
const mediaDir = path.join(__dirname, 'public', 'generated-media');
|
||||
if (!fs.existsSync(mediaDir)) {
|
||||
fs.mkdirSync(mediaDir, { recursive: true });
|
||||
}
|
||||
|
||||
const matches = imageUrl.match(/^data:image\/([A-Za-z+-]+);base64,(.+)$/);
|
||||
if (matches && matches.length === 3) {
|
||||
const ext = matches[1];
|
||||
const base64Data = matches[2];
|
||||
const fileName = `imagem_${Date.now()}.${ext === 'jpeg' ? 'jpg' : ext}`;
|
||||
const filePath = path.join(mediaDir, fileName);
|
||||
fs.writeFileSync(filePath, Buffer.from(base64Data, 'base64'));
|
||||
imageUrl = `/generated-media/${fileName}`;
|
||||
console.log(`[Smart Router] Imagem salva localmente em: ${imageUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
res.write(`data: ${JSON.stringify({ type: 'image', url: imageUrl, originalPrompt: promptText })}\n\n`);
|
||||
res.write('data: [DONE]\n\n');
|
||||
return res.end();
|
||||
@@ -565,7 +586,19 @@ app.post('/api/chat', requireAuth, async (req, res) => {
|
||||
|
||||
if (audioBase64Chunks.length > 0) {
|
||||
const fullAudioBase64 = audioBase64Chunks.join('');
|
||||
const audioUrl = `data:audio/mp3;base64,${fullAudioBase64}`;
|
||||
|
||||
// Salvar em arquivo no servidor
|
||||
const fs = require('fs');
|
||||
const mediaDir = path.join(__dirname, 'public', 'generated-media');
|
||||
if (!fs.existsSync(mediaDir)) {
|
||||
fs.mkdirSync(mediaDir, { recursive: true });
|
||||
}
|
||||
const fileName = `musica_${Date.now()}.mp3`;
|
||||
const filePath = path.join(mediaDir, fileName);
|
||||
fs.writeFileSync(filePath, Buffer.from(fullAudioBase64, 'base64'));
|
||||
|
||||
const audioUrl = `/generated-media/${fileName}`;
|
||||
console.log(`[Smart Router] Música salva localmente em: ${audioUrl}`);
|
||||
// Envia a mídia estruturada para o frontend
|
||||
res.write(`data: ${JSON.stringify({ type: 'audio', url: audioUrl, originalPrompt: promptText })}\n\n`);
|
||||
} else {
|
||||
@@ -589,23 +622,113 @@ app.post('/api/chat', requireAuth, async (req, res) => {
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// FLUXO DE VÍDEO (Mensagem Pedagógica Amigável - MiniMax Desativado)
|
||||
// FLUXO DE VÍDEO (google/veo-3.1-lite via OpenRouter)
|
||||
// ==========================================================================
|
||||
if (intent === 'VIDEO') {
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
|
||||
res.write(`data: ${JSON.stringify({ type: 'media_status', message: 'A generation de vídeos no momento está indisponível...' })}\n\n`);
|
||||
res.write(`data: ${JSON.stringify({ type: 'media_status', message: 'Iniciando geração de vídeo com o modelo google/veo-3.1-lite...' })}\n\n`);
|
||||
|
||||
try {
|
||||
const message = "A geração de vídeos no momento está indisponível devido às limitações do plano. No entanto, se você quiser, eu posso criar um roteiro pedagógico de vídeo ou de animação super detalhado para você! É só me dizer o tema que deseja abordar.";
|
||||
res.write(`data: ${JSON.stringify({ content: message })}\n\n`);
|
||||
// 1. Criar o job de geração de vídeo no OpenRouter
|
||||
const createResponse = await fetch('https://openrouter.ai/api/v1/videos', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'google/veo-3.1-lite',
|
||||
prompt: promptText
|
||||
})
|
||||
});
|
||||
|
||||
if (!createResponse.ok) {
|
||||
const errorText = await createResponse.ok ? '' : await createResponse.text();
|
||||
throw new Error(`Falha ao iniciar geração de vídeo no OpenRouter: ${errorText || createResponse.statusText}`);
|
||||
}
|
||||
|
||||
const jobData = await createResponse.json();
|
||||
const jobId = jobData.id;
|
||||
const pollingUrl = jobData.polling_url || `https://openrouter.ai/api/v1/videos/${jobId}`;
|
||||
|
||||
console.log(`[Smart Router] Job de vídeo iniciado no OpenRouter com ID: ${jobId}`);
|
||||
res.write(`data: ${JSON.stringify({ type: 'media_status', message: 'Vídeo enfileirado no OpenRouter, aguardando processamento...' })}\n\n`);
|
||||
|
||||
// 2. Polling loop para monitorar a conclusão do job
|
||||
let status = 'queued';
|
||||
let videoUrl = null;
|
||||
let attempts = 0;
|
||||
const maxAttempts = 30; // 3 minutos no total (6s por tentativa)
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
attempts++;
|
||||
// Aguarda 6 segundos antes de verificar novamente
|
||||
await new Promise(resolve => setTimeout(resolve, 6000));
|
||||
|
||||
console.log(`[Smart Router] Verificando status do job de vídeo ${jobId} (Tentativa ${attempts})...`);
|
||||
|
||||
const statusResponse = await fetch(pollingUrl, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!statusResponse.ok) {
|
||||
console.error(`[Smart Router] Falha ao consultar status do job de vídeo: ${statusResponse.statusText}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const statusData = await statusResponse.json();
|
||||
status = statusData.status;
|
||||
|
||||
if (status === 'completed') {
|
||||
videoUrl = statusData.unsigned_urls?.[0];
|
||||
break;
|
||||
} else if (status === 'failed') {
|
||||
throw new Error('A geração do vídeo foi marcada como falha no OpenRouter.');
|
||||
} else {
|
||||
// in_progress / queued / processing
|
||||
const progressPercent = Math.round((attempts / maxAttempts) * 100);
|
||||
res.write(`data: ${JSON.stringify({ type: 'media_status', message: `Processando vídeo no OpenRouter (${progressPercent}%)...` })}\n\n`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!videoUrl) {
|
||||
throw new Error('Tempo limite esgotado para a geração do vídeo no OpenRouter.');
|
||||
}
|
||||
|
||||
res.write(`data: ${JSON.stringify({ type: 'media_status', message: 'Vídeo concluído! Baixando arquivo para o servidor...' })}\n\n`);
|
||||
|
||||
// 3. Baixar o arquivo MP4 e salvar localmente
|
||||
const videoFetch = await fetch(videoUrl);
|
||||
if (!videoFetch.ok) {
|
||||
throw new Error('Falha ao efetuar download do arquivo de vídeo temporário');
|
||||
}
|
||||
|
||||
const videoBuffer = await videoFetch.arrayBuffer();
|
||||
const fs = require('fs');
|
||||
const mediaDir = path.join(__dirname, 'public', 'generated-media');
|
||||
if (!fs.existsSync(mediaDir)) {
|
||||
fs.mkdirSync(mediaDir, { recursive: true });
|
||||
}
|
||||
|
||||
const fileName = `video_${Date.now()}.mp4`;
|
||||
const filePath = path.join(mediaDir, fileName);
|
||||
fs.writeFileSync(filePath, Buffer.from(videoBuffer));
|
||||
|
||||
const localVideoUrl = `/generated-media/${fileName}`;
|
||||
console.log(`[Smart Router] Vídeo salvo localmente em: ${localVideoUrl}`);
|
||||
|
||||
res.write(`data: ${JSON.stringify({ type: 'video', url: localVideoUrl, originalPrompt: promptText })}\n\n`);
|
||||
res.write('data: [DONE]\n\n');
|
||||
return res.end();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erro na resposta de vídeo:', error);
|
||||
res.write(`data: ${JSON.stringify({ content: `Ocorreu um erro ao gerar o vídeo: ${error.message}` })}\n\n`);
|
||||
console.error('Erro na geração de vídeo:', error);
|
||||
res.write(`data: ${JSON.stringify({ content: `Desculpe, ocorreu um erro ao gerar o vídeo: ${error.message}.` })}\n\n`);
|
||||
res.write('data: [DONE]\n\n');
|
||||
return res.end();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user