From e41e3b6cbfec7d7a21a7b566b88527f720d8330b Mon Sep 17 00:00:00 2001 From: admtracksteel Date: Wed, 3 Jun 2026 02:44:17 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20migrar=20texto/imagem/m=C3=BAsica=20par?= =?UTF-8?q?a=20MiniMax=20M3=20+=20image-01=20+=20music-2.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adiciona helper callMinimax() para simplificar chamadas de texto - Chat principal: M3 primário (não-streaming com SSE adaptado) - Intent detector: M3 → OpenRouter → Groq (cascata) - Smart Report: M3 primário → OpenRouter → Groq (cascata) - Compilar Relatório: M3 primário → OpenRouter → Gemini (cascata) - Resumo de observações: M3 direto - Imagem: M3 reescreve prompt + MiniMax image-01 nativo - Música: M3 compõe letra/estilo + MiniMax music-2.0 nativo - Vídeo: MANTIDO no OpenRouter (MiniMax bloqueado no plano Starter) - Adiciona MINIMAX_MODEL=MiniMax-M3 no .env --- .env | 1 + server.js | 631 ++++++++++++++++++++++++++++++------------------------ 2 files changed, 357 insertions(+), 275 deletions(-) diff --git a/.env b/.env index e2b0504..13395ca 100644 --- a/.env +++ b/.env @@ -12,3 +12,4 @@ GOOGLE_AI_API_KEY=AIzaSyACelmI6-Bxc7TZ0sAzzgLd_F8VBexOjcQ GOOGLE_AI_MODEL=gemini-2.5-flash DATABASE_URL=postgresql://postgres:Xz0oyb6ArGYG5uAVTVwcvJxRrMuT7EIJ@supabase-db-h0oggskgs0ws0sco8kc4s8ws:5432/postgres +MINIMAX_MODEL=MiniMax-M3 diff --git a/server.js b/server.js index 09a9809..778d0b7 100644 --- a/server.js +++ b/server.js @@ -14,6 +14,48 @@ const multerUpload = multer({ limits: { fileSize: 25 * 1024 * 1024 } // 25MB max }); +// ============================================================================ +// HELPER: chamada à API MiniMax (texto / classificação / reescrita de prompt) +// Substitui OpenRouter para todas as funções de TEXTO a partir de jun/2026. +// Imagem, música e vídeo continuam usando endpoints nativos MiniMax em /v1/ +// ============================================================================ +async function callMinimax({ messages, temperature = 0.5, max_tokens = 1500, system = null, model = null }) { + const baseUrl = (process.env.MINIMAX_API_BASE || 'https://api.minimax.io/v1').replace(/\/v1\/?$/, ''); + const chosenModel = model || process.env.MINIMAX_MODEL || 'MiniMax-M3'; + const finalMessages = system + ? [{ role: 'system', content: system }, ...messages] + : messages; + + const res = await fetch(`${baseUrl}/anthropic/v1/messages`, { + method: 'POST', + headers: { + 'X-Api-Key': process.env.MINIMAX_API_KEY, + 'Content-Type': 'application/json', + 'anthropic-version': '2023-06-01' + }, + body: JSON.stringify({ + model: chosenModel, + max_tokens, + temperature, + messages: finalMessages + }) + }); + + if (!res.ok) { + const txt = await res.text(); + throw new Error(`MiniMax respondeu ${res.status}: ${txt}`); + } + + const data = await res.json(); + // Extrai só os blocos de texto (ignora thinking blocks) + const text = (data.content || []) + .filter(b => b.type === 'text') + .map(b => b.text) + .join('') + .trim(); + return { text, raw: data }; +} + app.use(express.json({ limit: '50mb' })); app.use(express.urlencoded({ limit: '50mb', extended: true })); app.use(cookieParser(process.env.SESSION_SECRET || 'camila-secret')); @@ -503,31 +545,46 @@ Retorne APENAS o JSON válido:`; let rawContent = ''; - // Tentar OpenRouter + // Tentar MiniMax (M3) como primário try { - console.log('[Smart Report] Tentando gerar relatório via OpenRouter...'); - const reportRes = 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: 'user', content: reportPrompt }], - temperature: 0.5, - max_tokens: 1500 - }) + console.log('[Smart Report] Tentando gerar relatório via MiniMax M3...'); + const result = await callMinimax({ + messages: [{ role: 'user', content: reportPrompt }], + temperature: 0.5, + max_tokens: 1500 }); - - if (reportRes.ok) { - const reportData = await reportRes.json(); - rawContent = reportData.choices?.[0]?.message?.content || ''; - } else { - console.warn('[Smart Report] Falha no OpenRouter:', await reportRes.text()); - } + rawContent = result.text; } catch (err) { - console.error('[Smart Report] Erro de conexão com OpenRouter:', err.message); + console.error('[Smart Report] Erro de conexão com MiniMax:', err.message); + } + + // Fallback para OpenRouter se MiniMax falhar + if (!rawContent && process.env.OPENROUTER_API_KEY) { + try { + console.log('[Smart Report] Usando OpenRouter como fallback...'); + const reportRes = 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: 'user', content: reportPrompt }], + temperature: 0.5, + max_tokens: 1500 + }) + }); + + if (reportRes.ok) { + const reportData = await reportRes.json(); + rawContent = reportData.choices?.[0]?.message?.content || ''; + } else { + console.warn('[Smart Report] Falha no OpenRouter:', await reportRes.text()); + } + } catch (err) { + console.error('[Smart Report] Erro de conexão com OpenRouter:', err.message); + } } // Fallback para Groq se OpenRouter falhar @@ -1005,30 +1062,45 @@ Orientações adicionais: let generatedReport = ''; - // Tenta OpenRouter + // Tenta MiniMax (M3) como primário try { - console.log('[Compilar Relatório] Chamando OpenRouter...'); - const response = 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: 'user', content: systemPrompt }], - temperature: 0.5, - max_tokens: 6000 - }) + console.log('[Compilar Relatório] Chamando MiniMax M3...'); + const result = await callMinimax({ + messages: [{ role: 'user', content: systemPrompt }], + temperature: 0.5, + max_tokens: 6000 }); - if (response.ok) { - const data = await response.json(); - generatedReport = data.choices?.[0]?.message?.content || ''; - } else { - console.warn('[Compilar Relatório] Falha no OpenRouter status:', response.status); - } + generatedReport = result.text; } catch (err) { - console.error('[Compilar Relatório] Erro no OpenRouter:', err.message); + console.error('[Compilar Relatório] Erro no MiniMax:', err.message); + } + + // Fallback para OpenRouter se MiniMax falhar + if (!generatedReport && process.env.OPENROUTER_API_KEY) { + try { + console.log('[Compilar Relatório] Fallback para OpenRouter...'); + const response = 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: 'user', content: systemPrompt }], + temperature: 0.5, + max_tokens: 6000 + }) + }); + if (response.ok) { + const data = await response.json(); + generatedReport = data.choices?.[0]?.message?.content || ''; + } else { + console.warn('[Compilar Relatório] Falha no OpenRouter status:', response.status); + } + } catch (err) { + console.error('[Compilar Relatório] Erro no OpenRouter:', err.message); + } } // Fallback para Google Gemini @@ -1246,22 +1318,12 @@ GERE UM RESUMO ESTRUTURADO COM: -Máximo 300 palavras -Seja objetivo e baseado nas evidências das observações`; - const compileRes = 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: 'user', content: compilePrompt }], - temperature: 0.4, - max_tokens: 800 - }) + const compileResult = await callMinimax({ + messages: [{ role: 'user', content: compilePrompt }], + temperature: 0.4, + max_tokens: 800 }); - - const compileData = await compileRes.json(); - const resumoText = compileData.choices?.[0]?.message?.content || ''; + const resumoText = compileResult.text; res.json({ crianca, @@ -1851,39 +1913,63 @@ app.post('/api/chat', requireAuth, async (req, res) => { } // Executar classificação automática apenas se não houver intenção forçada, não houver imagens anexadas e houver prompt - if (!forcedIntent && !hasAttachedImages && promptText && (process.env.OPENROUTER_API_KEY || process.env.GROQ_API_KEY)) { + if (!forcedIntent && !hasAttachedImages && promptText && (process.env.MINIMAX_API_KEY || process.env.OPENROUTER_API_KEY || process.env.GROQ_API_KEY)) { try { const systemDetector = "Você é um classificador de intenções de prompt. Classifique a última mensagem do usuário em uma destas quatro categorias exatas: 'TEXT' (para conversas normais, perguntas, redação de relatórios, planejamentos, textos pedagógicos, respostas gerais), 'IMAGE' (se o usuário estiver pedindo explicitamente para gerar, desenhar, criar ou ilustrar uma imagem, figura, ilustração ou foto), 'AUDIO' (se o usuário estiver pedindo explicitamente para gerar uma música, melodia, trilha sonora, cantiga ou som), ou 'VIDEO' (se o usuário estiver pedindo explicitamente para gerar ou criar um vídeo ou animação). Responda APENAS com a palavra da categoria em letras maiúsculas: TEXT, IMAGE, AUDIO, VIDEO."; - let detectResponse; + let detectedText = null; let usingGroqForDetection = false; - try { - detectResponse = 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: systemDetector }, - { role: 'user', content: promptText } - ], - max_tokens: 20, - temperature: 0.0 - }) - }); - } catch (err) { - console.error('[Smart Router] Falha de conexão com OpenRouter para detecção, tentando Groq...', err.message); + // 1) Tentar MiniMax M3 como primário + if (process.env.MINIMAX_API_KEY) { + try { + console.log('[Smart Router] Detectando intenção via MiniMax M3...'); + const r = await callMinimax({ + messages: [{ role: 'user', content: promptText }], + system: systemDetector, + temperature: 0.0, + max_tokens: 20 + }); + detectedText = r.text; + } catch (err) { + console.error('[Smart Router] Falha na detecção via MiniMax, tentando OpenRouter...', err.message); + } } - // Se OpenRouter falhou ou retornou erro (ex: status != 200), tenta o Groq como fallback - if ((!detectResponse || !detectResponse.ok) && process.env.GROQ_API_KEY) { - console.log('[Smart Router] Usando Groq (llama-3.1-8b-instant) para classificar intenção...'); + // 2) Fallback: OpenRouter + if (!detectedText && process.env.OPENROUTER_API_KEY) { try { - detectResponse = await fetch('https://api.groq.com/openai/v1/chat/completions', { + console.log('[Smart Router] Detectando intenção via OpenRouter...'); + const detectResponse = 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: systemDetector }, + { role: 'user', content: promptText } + ], + max_tokens: 20, + temperature: 0.0 + }) + }); + if (detectResponse.ok) { + const detectData = await detectResponse.json(); + detectedText = detectData.choices?.[0]?.message?.content?.toUpperCase() || null; + } + } catch (err) { + console.error('[Smart Router] Falha de detecção na OpenRouter, tentando Groq...', err.message); + } + } + + // 3) Fallback final: Groq + if (!detectedText && process.env.GROQ_API_KEY) { + console.log('[Smart Router] Detectando intenção via Groq (llama-3.1-8b-instant)...'); + try { + const detectResponse = await fetch('https://api.groq.com/openai/v1/chat/completions', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.GROQ_API_KEY}`, @@ -1899,30 +1985,32 @@ app.post('/api/chat', requireAuth, async (req, res) => { temperature: 0.0 }) }); - usingGroqForDetection = detectResponse.ok; + if (detectResponse.ok) { + const detectData = await detectResponse.json(); + detectedText = detectData.choices?.[0]?.message?.content?.toUpperCase() || null; + usingGroqForDetection = !!detectedText; + } } catch (groqErr) { console.error('[Smart Router] Falha de detecção na API da Groq:', groqErr.message); } } - if (detectResponse && detectResponse.ok) { - const detectData = await detectResponse.json(); - const detectedText = detectData.choices?.[0]?.message?.content?.toUpperCase() || 'TEXT'; - console.log(`[Smart Router] Resposta crua do detector: "${detectedText}"`); + if (detectedText) { + const txt = detectedText.toUpperCase(); + console.log(`[Smart Router] Resposta crua do detector: "${txt}"`); - if (detectedText.includes('IMAGE')) { + if (txt.includes('IMAGE')) { intent = 'IMAGE'; - } else if (detectedText.includes('AUDIO') || detectedText.includes('MUSIC')) { + } else if (txt.includes('AUDIO') || txt.includes('MUSIC')) { intent = 'AUDIO'; - } else if (detectedText.includes('VIDEO')) { + } else if (txt.includes('VIDEO')) { intent = 'VIDEO'; } else { intent = 'TEXT'; } console.log(`[Smart Router] Intenção detectada para o prompt "${promptText.substring(0, 40)}...": ${intent}`); } else { - const errText = await detectResponse.text(); - console.error('[Smart Router] Chamada de detecção retornou erro:', errText); + console.error('[Smart Router] Nenhum detector respondeu; usando TEXT como fallback.'); } } catch (err) { console.error('Erro no classificador de intenções (usando fallback TEXT):', err); @@ -1930,95 +2018,77 @@ app.post('/api/chat', requireAuth, async (req, res) => { } // ========================================================================== - // FLUXO DE IMAGEM (MiniMax image-01) + // FLUXO DE IMAGEM (MiniMax M3 + image-01) // ========================================================================== if (intent === 'IMAGE') { 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 imagem com o OpenRouter...' })}\n\n`); + res.write(`data: ${JSON.stringify({ type: 'media_status', message: 'Iniciando geração de imagem com o MiniMax...' })}\n\n`); try { - // Prompt rewriting: pedir à IA para enriquecer o prompt em inglês + // Prompt rewriting: pedir ao M3 para enriquecer o prompt em inglês 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 imagem do usuário em inglês como um prompt altamente detalhado para geradores de imagem (como Midjourney/Stable Diffusion). Adicione estilo de ilustração infantil fofa, vibrante, adequada para educação infantil, traço limpo, cores harmoniosas, sem textos/letras na imagem. Retorne apenas a tradução/prompt em inglês." }, - { role: 'user', content: promptText } - ], - max_tokens: 150, - temperature: 0.7 - }) + const r = await callMinimax({ + system: "Reescreva o pedido de imagem do usuário em inglês como um prompt altamente detalhado para geradores de imagem (como Midjourney/Stable Diffusion). Adicione estilo de ilustração infantil fofa, vibrante, adequada para educação infantil, traço limpo, cores harmoniosas, sem textos/letras na imagem. Retorne apenas a tradução/prompt em inglês.", + messages: [{ role: 'user', content: promptText }], + temperature: 0.7, + max_tokens: 200 }); - if (rewriteResp.ok) { - const rewriteData = await rewriteResp.json(); - enrichedPrompt = rewriteData.choices?.[0]?.message?.content?.trim() || promptText; - console.log('[Smart Router] Prompt reescrito para imagem:', enrichedPrompt); - } + enrichedPrompt = r.text || promptText; + console.log('[Smart Router] Prompt reescrito para imagem (M3):', enrichedPrompt); } catch (err) { - console.error('Erro ao reescrever prompt de imagem:', err); + console.error('Erro ao reescrever prompt de imagem via M3:', err.message); } - res.write(`data: ${JSON.stringify({ type: 'media_status', message: 'Gerando ilustração pedagógica com o OpenRouter...' })}\n\n`); + res.write(`data: ${JSON.stringify({ type: 'media_status', message: 'Gerando ilustração pedagógica com MiniMax image-01...' })}\n\n`); - const response = await fetch('https://openrouter.ai/api/v1/chat/completions', { + // 1) Gerar imagem via endpoint nativo da MiniMax + 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.OPENROUTER_API_KEY}`, + 'Authorization': `Bearer ${process.env.MINIMAX_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ - model: 'x-ai/grok-imagine-image-quality', - messages: [ - { role: 'user', content: enrichedPrompt } - ], - modalities: ['image'] + model: 'image-01', + prompt: enrichedPrompt, + n: 1 }) }); - if (!response.ok) { - const errText = await response.text(); - throw new Error(`OpenRouter respondeu com erro ao gerar imagem: ${errText}`); + if (!genResp.ok) { + const errText = await genResp.text(); + throw new Error(`MiniMax image-01 respondeu erro: ${errText}`); } - const imgData = await response.json(); - const imageObj = imgData.choices?.[0]?.message?.images?.[0]; - let imageUrl = imageObj?.image_url?.url; - + const genData = await genResp.json(); + const imageUrl = genData.data?.image_urls?.[0]; if (!imageUrl) { - throw new Error('Nenhuma imagem foi retornada pelo OpenRouter'); + throw new Error('Nenhuma URL de imagem retornada pela MiniMax'); } - // 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}`); - } + // 2) Baixar e salvar localmente no servidor (a URL expira em pouco tempo) + const fs = require('fs'); + 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 = `imagem_${Date.now()}.jpg`; + const filePath = path.join(mediaDir, fileName); + fs.writeFileSync(filePath, imgBuffer); + const localUrl = `/generated-media/${fileName}`; + console.log(`[Smart Router] Imagem salva em: ${localUrl}`); - res.write(`data: ${JSON.stringify({ type: 'image', url: imageUrl, originalPrompt: promptText })}\n\n`); + res.write(`data: ${JSON.stringify({ type: 'image', url: localUrl, originalPrompt: promptText })}\n\n`); res.write('data: [DONE]\n\n'); return res.end(); @@ -2031,140 +2101,99 @@ app.post('/api/chat', requireAuth, async (req, res) => { } // ========================================================================== - // FLUXO DE ÁUDIO/MÚSICA (Mensagem Pedagógica Amigável - MiniMax Desativado) - // ========================================================================== - // ========================================================================== - // FLUXO DE ÁUDIO/MÚSICA (google/lyria-3-clip-preview) + // FLUXO DE ÁUDIO/MÚSICA (MiniMax music-2.0) // ========================================================================== 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 e gerando música pedagógica com Lyria AI...' })}\n\n`); + res.write(`data: ${JSON.stringify({ type: 'media_status', message: 'Compondo e gerando música pedagógica com MiniMax music-2.0...' })}\n\n`); try { - if (!process.env.OPENROUTER_API_KEY) { - throw new Error('OPENROUTER_API_KEY não configurada no servidor.'); + if (!process.env.MINIMAX_API_KEY) { + throw new Error('MINIMAX_API_KEY não configurada no servidor.'); } - const response = await fetch('https://openrouter.ai/api/v1/chat/completions', { + // Estratégia: usar o M3 para gerar a letra (em PT-BR) e um prompt descritivo do estilo, + // depois enviar pro music-2.0 gerar a música. + let lyrics = ''; + let stylePrompt = ''; + try { + const r = await callMinimax({ + system: "Você é um compositor pedagógico infantil. Para o pedido do usuário, gere APENAS um JSON válido com duas chaves: 'lyrics' (letra curta em português brasileiro, de 8 a 20 versos, rimada, alegre, repetitiva, com vocabulário adequado para educação infantil) e 'style' (descrição curta em inglês do estilo musical, ex: 'happy acoustic children song, ukulele, claps, 100 BPM'). Responda somente o JSON.", + messages: [{ role: 'user', content: promptText }], + temperature: 0.7, + max_tokens: 400 + }); + const txt = r.text || ''; + // Tentar extrair JSON mesmo se vier com lixo + const m = txt.match(/\{[\s\S]*\}/); + if (m) { + const parsed = JSON.parse(m[0]); + lyrics = parsed.lyrics || ''; + stylePrompt = parsed.style || ''; + } + } catch (err) { + console.error('[Smart Router] Falha ao gerar letra/estilo via M3:', err.message); + } + + // Fallback simples se M3 não produziu + if (!lyrics) { + lyrics = `Vamos brincar e aprender,\nCom alegria vamos cantar,\n${promptText}\nÉ divertido estudar!`; + } + if (!stylePrompt) { + stylePrompt = 'happy acoustic children song, ukulele, claps, 100 BPM'; + } + + res.write(`data: ${JSON.stringify({ content: `🎵 *Compondo sua cantiga...* \n\n${lyrics}\n\n` })}\n\n`); + + // 1) Gerar música via endpoint nativo MiniMax music-2.0 + const minmBase = (process.env.MINIMAX_API_BASE || 'https://api.minimax.io/v1').replace(/\/v1\/?$/, ''); + const musicResp = await fetch(`${minmBase}/v1/music_generation`, { method: 'POST', headers: { - 'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`, - 'HTTP-Referer': 'https://camila.reifonas.cloud', - 'X-Title': 'Camila ChatGPT replica', + 'Authorization': `Bearer ${process.env.MINIMAX_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ - model: 'google/lyria-3-clip-preview', - messages: [ - { role: 'user', content: promptText } - ], - modalities: ['audio'], - stream: true + model: 'music-2.0', + prompt: stylePrompt, + lyrics: lyrics, + audio_setting: { sample_rate: 32000, bitrate: 128000, format: 'mp3' } }) }); - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Erro na API do OpenRouter (Lyria): ${errorText}`); + if (!musicResp.ok) { + const errText = await musicResp.text(); + throw new Error(`MiniMax music-2.0 respondeu erro: ${errText}`); } - const reader = response.body.getReader(); - const decoder = new TextDecoder('utf-8'); - let buffer = ''; - let audioBase64Chunks = []; - let textContent = ''; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split('\n'); - buffer = lines.pop(); - - for (const line of lines) { - const cleanLine = line.trim(); - if (cleanLine.startsWith('data: ')) { - const dataContent = cleanLine.slice(6); - if (dataContent === '[DONE]') continue; - try { - const parsed = JSON.parse(dataContent); - const delta = parsed.choices?.[0]?.delta; - if (delta) { - if (delta.content) { - textContent += delta.content; - res.write(`data: ${JSON.stringify({ content: delta.content })}\n\n`); - } - if (delta.audio?.data) { - audioBase64Chunks.push(delta.audio.data); - } - } - } catch (e) { - // Ignorar - } - } - } + const musicData = await musicResp.json(); + // O endpoint pode retornar audio em base64 dentro de data.audio + const audioHex = musicData.data?.audio; + if (!audioHex) { + throw new Error('Nenhum dado de áudio retornado pela MiniMax music-2.0'); } - // Processar restante do buffer - if (buffer) { - const cleanLine = buffer.trim(); - if (cleanLine.startsWith('data: ')) { - const dataContent = cleanLine.slice(6); - if (dataContent !== '[DONE]') { - try { - const parsed = JSON.parse(dataContent); - const delta = parsed.choices?.[0]?.delta; - if (delta) { - if (delta.content) { - textContent += delta.content; - res.write(`data: ${JSON.stringify({ content: delta.content })}\n\n`); - } - if (delta.audio?.data) { - audioBase64Chunks.push(delta.audio.data); - } - } - } catch (e) { - // Ignorar - } - } - } - } - - if (audioBase64Chunks.length > 0) { - const fullAudioBase64 = audioBase64Chunks.join(''); - - // 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 { - // Fallback se não retornou áudio (apenas texto) - if (textContent) { - res.write(`data: ${JSON.stringify({ content: '\n\n*(Nota: O áudio não pôde ser gerado por limitações do modelo, mas a letra acima foi composta com carinho!)*' })}\n\n`); - } else { - throw new Error('Nenhum dado de áudio ou texto foi retornado pelo modelo Lyria.'); - } + // 2) Salvar arquivo local + 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(audioHex, 'hex')); + const audioUrl = `/generated-media/${fileName}`; + console.log(`[Smart Router] Música salva em: ${audioUrl}`); + res.write(`data: ${JSON.stringify({ type: 'audio', url: audioUrl, originalPrompt: promptText })}\n\n`); res.write('data: [DONE]\n\n'); return res.end(); } catch (error) { - console.error('Erro na resposta de áudio com Lyria:', error); + console.error('Erro na geração de música:', error); res.write(`data: ${JSON.stringify({ content: `Desculpe, ocorreu um erro ao gerar a música: ${error.message}. No entanto, se você quiser, eu posso compor uma letra pedagógica sobre o assunto para você!` })}\n\n`); res.write('data: [DONE]\n\n'); return res.end(); @@ -2366,26 +2395,77 @@ app.post('/api/chat', requireAuth, async (req, res) => { try { let chatResponse; - let providerUsed = 'OpenRouter'; + let providerUsed = 'MiniMax-M3'; + // 1) Tentar MiniMax M3 (NÃO-STREAMING para o M3 porque o M3 devolve thinking blocks + // que não seguem o protocolo SSE do OpenAI; a UI consome o JSON final e faz o streaming + // via o endpoint /v1/chat/completions que vamos montar localmente). + let useMinimaxNonStream = false; try { - chatResponse = await fetch('https://openrouter.ai/api/v1/chat/completions', { + console.log('[Smart Router] Chamando MiniMax M3 (não-streaming, JSON final)...'); + const minmBase = (process.env.MINIMAX_API_BASE || 'https://api.minimax.io/v1').replace(/\/v1\/?$/, ''); + const minmResp = await fetch(`${minmBase}/anthropic/v1/messages`, { method: 'POST', headers: { - 'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`, - 'HTTP-Referer': 'https://camila.reifonas.cloud', - 'X-Title': 'Camila ChatGPT replica', - 'Content-Type': 'application/json' + 'X-Api-Key': process.env.MINIMAX_API_KEY, + 'Content-Type': 'application/json', + 'anthropic-version': '2023-06-01' }, body: JSON.stringify({ - model: process.env.OPENROUTER_MODEL || 'openai/gpt-4.1-nano', - messages: enrichedMessages, - stream: true, - temperature + model: process.env.MINIMAX_MODEL || 'MiniMax-M3', + max_tokens: 4096, + temperature, + system: enrichedMessages[0].content, + messages: enrichedMessages.slice(1) }) }); + if (minmResp.ok) { + const data = await minmResp.json(); + const text = (data.content || []).filter(b => b.type === 'text').map(b => b.text).join('').trim(); + if (text) { + // "Transformar" resposta JSON em um objeto compatível com o resto do fluxo: + // devolvemos um Response falso com stream SSE + const sseBody = + `data: ${JSON.stringify({ choices: [{ delta: { content: text } }] })}\n\n` + + `data: [DONE]\n\n`; + chatResponse = new Response(sseBody, { + status: 200, + headers: { 'Content-Type': 'text/event-stream' } + }); + useMinimaxNonStream = true; + } else { + console.warn('[Smart Router] M3 respondeu OK mas sem texto; caindo no OpenRouter.'); + } + } else { + console.warn('[Smart Router] M3 status', minmResp.status, '- caindo no OpenRouter.'); + } } catch (e) { - console.error('Erro na conexão com OpenRouter, tentando Groq...', e.message); + console.error('Erro na conexão com MiniMax M3, tentando OpenRouter...', e.message); + } + + // 2) Fallback: OpenRouter (streaming SSE, igual ao original) + if ((!chatResponse || !chatResponse.ok) && !useMinimaxNonStream && process.env.OPENROUTER_API_KEY) { + console.log('[Smart Router] Usando OpenRouter como fallback de streaming de texto...'); + providerUsed = 'OpenRouter'; + try { + chatResponse = await fetch('https://openrouter.ai/api/v1/chat/completions', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`, + 'HTTP-Referer': 'https://camila.reifonas.cloud', + 'X-Title': 'Camila ChatGPT replica', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model: process.env.OPENROUTER_MODEL || 'openai/gpt-4.1-nano', + messages: enrichedMessages, + stream: true, + temperature + }) + }); + } catch (e) { + console.error('Erro na conexão com OpenRouter, tentando Groq...', e.message); + } } // Se o OpenRouter falhar ou retornar erro (ex: status != 200), caímos no fallback do Groq @@ -2434,26 +2514,27 @@ app.post('/api/chat', requireAuth, async (req, res) => { } } - // Se o OpenRouter, Groq e Google Gemini falharem ou retornarem erro, caímos no fallback do MiniMax M2.7 + // Se o OpenRouter, Groq e Google Gemini falharem ou retornarem erro, caímos no fallback do MiniMax M3 (streaming) if ((!chatResponse || !chatResponse.ok) && process.env.MINIMAX_API_KEY) { - console.log('[Smart Router] Usando MiniMax-M2.7 como fallback de streaming de texto...'); - providerUsed = 'MiniMax M2.7'; + console.log('[Smart Router] Usando MiniMax-M3 (streaming OpenAI-compat) como último fallback...'); + providerUsed = 'MiniMax-M3'; try { - chatResponse = await fetch('https://api.minimaxi.chat/v1/chat/completions', { + const minmBase = (process.env.MINIMAX_API_BASE || 'https://api.minimax.io/v1').replace(/\/v1\/?$/, ''); + chatResponse = await fetch(`${minmBase}/v1/chat/completions`, { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.MINIMAX_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ - model: 'MiniMax-M2.7', + model: process.env.MINIMAX_MODEL || 'MiniMax-M3', messages: enrichedMessages, stream: true, temperature }) }); } catch (minimaxErr) { - console.error('[Smart Router] Erro crítico: Falha no fallback do MiniMax M2.7 também:', minimaxErr.message); + console.error('[Smart Router] Erro crítico: Falha no fallback do MiniMax M3 também:', minimaxErr.message); } }