Refatoração Smart Router: migração MiniMax para OpenRouter (imagem) e fallback para texto amigável em áudio/vídeo
This commit is contained in:
@@ -356,10 +356,10 @@ app.post('/api/chat', requireAuth, async (req, res) => {
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
|
||||
res.write(`data: ${JSON.stringify({ type: 'media_status', message: 'Iniciando geração de imagem com a MiniMax...' })}\n\n`);
|
||||
res.write(`data: ${JSON.stringify({ type: 'media_status', message: 'Iniciando geração de imagem com o OpenRouter...' })}\n\n`);
|
||||
|
||||
try {
|
||||
// Prompt rewriting: pedir à IA para enriquecer o prompt em inglês para a MiniMax
|
||||
// Prompt rewriting: pedir à IA para enriquecer o prompt em inglês
|
||||
let enrichedPrompt = promptText;
|
||||
try {
|
||||
const rewriteResp = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||||
@@ -387,33 +387,34 @@ app.post('/api/chat', requireAuth, async (req, res) => {
|
||||
console.error('Erro ao reescrever prompt de imagem:', err);
|
||||
}
|
||||
|
||||
res.write(`data: ${JSON.stringify({ type: 'media_status', message: 'Gerando ilustração pedagógica...' })}\n\n`);
|
||||
res.write(`data: ${JSON.stringify({ type: 'media_status', message: 'Gerando ilustração pedagógica com o OpenRouter...' })}\n\n`);
|
||||
|
||||
const response = await fetch(`${process.env.MINIMAX_API_BASE}/image_generation`, {
|
||||
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.MINIMAX_API_KEY}`,
|
||||
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'image-01',
|
||||
prompt: enrichedPrompt,
|
||||
aspect_ratio: '1:1',
|
||||
response_format: 'url'
|
||||
model: 'google/gemini-2.5-flash-image',
|
||||
messages: [
|
||||
{ role: 'user', content: enrichedPrompt }
|
||||
],
|
||||
modalities: ['image']
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errText = await response.text();
|
||||
throw new Error(`MiniMax respondeu com erro: ${errText}`);
|
||||
throw new Error(`OpenRouter respondeu com erro ao gerar imagem: ${errText}`);
|
||||
}
|
||||
|
||||
const imgData = await response.json();
|
||||
const imageUrl = imgData.data?.image_url?.[0];
|
||||
const imageObj = imgData.choices?.[0]?.message?.images?.[0];
|
||||
const imageUrl = imageObj?.image_url?.url;
|
||||
|
||||
if (!imageUrl) {
|
||||
const baseRespMsg = imgData.base_resp?.status_msg || (imgData.base_resp?.status_code ? `Erro MiniMax (Código: ${imgData.base_resp.status_code})` : null);
|
||||
throw new Error(baseRespMsg || 'Nenhuma imagem foi gerada pela API do MiniMax');
|
||||
throw new Error('Nenhuma imagem foi retornada pelo OpenRouter');
|
||||
}
|
||||
|
||||
res.write(`data: ${JSON.stringify({ type: 'image', url: imageUrl, originalPrompt: promptText })}\n\n`);
|
||||
@@ -429,91 +430,22 @@ app.post('/api/chat', requireAuth, async (req, res) => {
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// FLUXO DE ÁUDIO/MÚSICA (MiniMax music-2.6)
|
||||
// FLUXO DE ÁUDIO/MÚSICA (Mensagem Pedagógica Amigável - MiniMax Desativado)
|
||||
// ==========================================================================
|
||||
if (intent === 'AUDIO') {
|
||||
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: 'Compondo a letra da música infantil...' })}\n\n`);
|
||||
res.write(`data: ${JSON.stringify({ type: 'media_status', message: 'A geração de músicas no momento está indisponível...' })}\n\n`);
|
||||
|
||||
try {
|
||||
// Gerar letra curta e estilo musical com OpenRouter
|
||||
let lyrics = '';
|
||||
let stylePrompt = '';
|
||||
try {
|
||||
const composerResp = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: process.env.OPENROUTER_MODEL || 'openai/gpt-4.1-nano',
|
||||
messages: [
|
||||
{ role: 'system', content: "Crie uma letra de música infantil em português baseada no prompt do usuário. A música deve ser muito curta (apenas 2 estrofes e 1 refrão) usando as tags estruturais [Verse] e [Chorus]. Crie também um estilo de trilha adequado em inglês (ex: 'Acoustic children song, happy, acoustic guitar, soft piano, female vocals'). Retorne obrigatoriamente um JSON no formato: {\"lyrics\": \"[Verse]...\", \"style\": \"style in english\"}" },
|
||||
{ role: 'user', content: promptText }
|
||||
],
|
||||
response_format: { type: 'json_object' }
|
||||
})
|
||||
});
|
||||
|
||||
if (composerResp.ok) {
|
||||
const compData = await composerResp.json();
|
||||
const parsedComp = JSON.parse(compData.choices?.[0]?.message?.content || '{}');
|
||||
lyrics = parsedComp.lyrics || `[Verse]\n${promptText}`;
|
||||
stylePrompt = parsedComp.style || 'acoustic children song, happy, female vocals';
|
||||
console.log('[Smart Router] Letra e estilo compostos:', { stylePrompt, lyrics });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Erro ao compor letra de música:', err);
|
||||
lyrics = `[Verse]\n${promptText}`;
|
||||
stylePrompt = 'children song, acoustic guitar, female vocals';
|
||||
}
|
||||
|
||||
res.write(`data: ${JSON.stringify({ type: 'media_status', message: 'Gerando áudio da música com a MiniMax...' })}\n\n`);
|
||||
|
||||
const response = await fetch(`${process.env.MINIMAX_API_BASE}/music_generation`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.MINIMAX_API_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'music-2.6',
|
||||
prompt: stylePrompt,
|
||||
lyrics: lyrics,
|
||||
is_instrumental: false,
|
||||
lyrics_optimizer: true
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errText = await response.text();
|
||||
throw new Error(`MiniMax respondeu com erro na música: ${errText}`);
|
||||
}
|
||||
|
||||
const musicData = await response.json();
|
||||
let audioUrl = musicData.data?.audio_url;
|
||||
|
||||
// Se a API retornar os dados binários codificados em hex
|
||||
if (!audioUrl && musicData.data?.audio) {
|
||||
const audioBase64 = Buffer.from(musicData.data.audio, 'hex').toString('base64');
|
||||
audioUrl = `data:audio/mp3;base64,${audioBase64}`;
|
||||
}
|
||||
|
||||
if (!audioUrl) {
|
||||
const baseRespMsg = musicData.base_resp?.status_msg || (musicData.base_resp?.status_code ? `Erro MiniMax (Código: ${musicData.base_resp.status_code})` : null);
|
||||
throw new Error(baseRespMsg || 'Nenhum áudio de música foi gerado pela API da MiniMax');
|
||||
}
|
||||
|
||||
res.write(`data: ${JSON.stringify({ type: 'audio', url: audioUrl, originalPrompt: promptText })}\n\n`);
|
||||
const message = "A geração de músicas no momento está indisponível devido às limitações do plano. No entanto, se você quiser, eu posso compor uma linda letra de música infantil ou poesia pedagógica sobre o assunto para você! É só me dizer sobre o que quer falar.";
|
||||
res.write(`data: ${JSON.stringify({ content: message })}\n\n`);
|
||||
res.write('data: [DONE]\n\n');
|
||||
return res.end();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erro na geração de música:', error);
|
||||
console.error('Erro na resposta de áudio:', error);
|
||||
res.write(`data: ${JSON.stringify({ content: `Ocorreu um erro ao compor a música: ${error.message}` })}\n\n`);
|
||||
res.write('data: [DONE]\n\n');
|
||||
return res.end();
|
||||
@@ -521,126 +453,22 @@ app.post('/api/chat', requireAuth, async (req, res) => {
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// FLUXO DE VÍDEO (MiniMax video-01 - Assíncrono)
|
||||
// FLUXO DE VÍDEO (Mensagem Pedagógica Amigável - MiniMax Desativado)
|
||||
// ==========================================================================
|
||||
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: 'Iniciando geração de vídeo na MiniMax...' })}\n\n`);
|
||||
res.write(`data: ${JSON.stringify({ type: 'media_status', message: 'A generation de vídeos no momento está indisponível...' })}\n\n`);
|
||||
|
||||
try {
|
||||
// Prompt rewriting em inglês para vídeo
|
||||
let enrichedPrompt = promptText;
|
||||
try {
|
||||
const rewriteResp = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: process.env.OPENROUTER_MODEL || 'openai/gpt-4.1-nano',
|
||||
messages: [
|
||||
{ role: 'system', content: "Reescreva o pedido de vídeo do usuário em inglês como um prompt altamente descritivo e fluído para geradores de vídeo. Adicione estilo de animação 3D amigável, cores vibrantes, infantil, movimentos suaves. Retorne apenas o prompt em inglês." },
|
||||
{ role: 'user', content: promptText }
|
||||
],
|
||||
max_tokens: 150,
|
||||
temperature: 0.7
|
||||
})
|
||||
});
|
||||
if (rewriteResp.ok) {
|
||||
const rewriteData = await rewriteResp.json();
|
||||
enrichedPrompt = rewriteData.choices?.[0]?.message?.content?.trim() || promptText;
|
||||
console.log('[Smart Router] Prompt reescrito para vídeo:', enrichedPrompt);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Erro ao reescrever prompt de vídeo:', err);
|
||||
}
|
||||
|
||||
res.write(`data: ${JSON.stringify({ type: 'media_status', message: 'Criando tarefa de renderização...' })}\n\n`);
|
||||
|
||||
const response = await fetch(`${process.env.MINIMAX_API_BASE}/video_generation`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.MINIMAX_API_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'video-01',
|
||||
prompt: enrichedPrompt
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errText = await response.text();
|
||||
throw new Error(`MiniMax respondeu com erro na geração de vídeo: ${errText}`);
|
||||
}
|
||||
|
||||
const jobData = await response.json();
|
||||
const taskId = jobData.task_id;
|
||||
|
||||
if (!taskId) {
|
||||
const baseRespMsg = jobData.base_resp?.status_msg || (jobData.base_resp?.status_code ? `Erro MiniMax (Código: ${jobData.base_resp.status_code})` : null);
|
||||
throw new Error(baseRespMsg || 'Nenhum ID de tarefa foi retornado pelo MiniMax');
|
||||
}
|
||||
|
||||
// Polling do status do vídeo
|
||||
let videoUrl = null;
|
||||
let attempts = 0;
|
||||
const maxAttempts = 30; // ~90 segundos máximo (3 segundos de intervalo)
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
res.write(`data: ${JSON.stringify({ type: 'media_status', message: `Processando vídeo... (${attempts * 3}s / 90s)` })}\n\n`);
|
||||
|
||||
const checkResp = await fetch(`${process.env.MINIMAX_API_BASE}/query/video_generation?task_id=${taskId}`, {
|
||||
headers: { 'Authorization': `Bearer ${process.env.MINIMAX_API_KEY}` }
|
||||
});
|
||||
|
||||
if (checkResp.ok) {
|
||||
const checkData = await checkResp.json();
|
||||
console.log(`[Smart Router] Status do vídeo job ${taskId}:`, checkData.status);
|
||||
|
||||
if (checkData.status === 'Success') {
|
||||
const fileId = checkData.file_id;
|
||||
|
||||
// Obter link de download real via Files API
|
||||
const fileResp = await fetch(`${process.env.MINIMAX_API_BASE}/files/retrieve?file_id=${fileId}`, {
|
||||
headers: { 'Authorization': `Bearer ${process.env.MINIMAX_API_KEY}` }
|
||||
});
|
||||
|
||||
if (fileResp.ok) {
|
||||
const fileData = await fileResp.json();
|
||||
videoUrl = fileData.file?.download_url;
|
||||
if (!videoUrl) {
|
||||
const baseRespMsg = fileData.base_resp?.status_msg || 'Falha ao recuperar download URL do arquivo de vídeo';
|
||||
throw new Error(baseRespMsg);
|
||||
}
|
||||
} else {
|
||||
const errText = await fileResp.text();
|
||||
throw new Error(`Falha ao obter arquivo do vídeo: ${errText}`);
|
||||
}
|
||||
break;
|
||||
} else if (checkData.status === 'Fail') {
|
||||
throw new Error(checkData.base_resp?.status_msg || 'A API da MiniMax falhou em renderizar este vídeo.');
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
attempts++;
|
||||
}
|
||||
|
||||
if (!videoUrl) {
|
||||
throw new Error('Tempo limite de renderização de vídeo excedido');
|
||||
}
|
||||
|
||||
res.write(`data: ${JSON.stringify({ type: 'video', url: videoUrl, originalPrompt: promptText })}\n\n`);
|
||||
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`);
|
||||
res.write('data: [DONE]\n\n');
|
||||
return res.end();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erro na geração de vídeo:', 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`);
|
||||
res.write('data: [DONE]\n\n');
|
||||
return res.end();
|
||||
|
||||
Reference in New Issue
Block a user