feat: implementa geracao de musica com google/lyria-3-clip-preview via OpenRouter

This commit is contained in:
2026-05-26 17:43:22 +00:00
parent 8a96b17c82
commit 102aa6d712
2 changed files with 195 additions and 11 deletions
+112 -6
View File
@@ -270,7 +270,7 @@ app.post('/api/chat', requireAuth, async (req, res) => {
let intent = 'TEXT';
// Executar classificação apenas se houver prompt de texto e não houver imagens anexadas
if (!hasAttachedImages && promptText && process.env.MINIMAX_API_KEY) {
if (!hasAttachedImages && promptText && (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.";
@@ -432,21 +432,127 @@ 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)
// ==========================================================================
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: 'A geração de músicas no momento está indisponível...' })}\n\n`);
res.write(`data: ${JSON.stringify({ type: 'media_status', message: 'Compondo e gerando música pedagógica com Lyria AI...' })}\n\n`);
try {
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`);
if (!process.env.OPENROUTER_API_KEY) {
throw new Error('OPENROUTER_API_KEY não configurada no servidor.');
}
const response = 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: 'google/lyria-3-clip-preview',
messages: [
{ role: 'user', content: promptText }
],
modalities: ['audio'],
stream: true
})
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Erro na API do OpenRouter (Lyria): ${errorText}`);
}
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
}
}
}
}
// 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('');
const audioUrl = `data:audio/mp3;base64,${fullAudioBase64}`;
// 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.');
}
}
res.write('data: [DONE]\n\n');
return res.end();
} catch (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`);
console.error('Erro na resposta de áudio com Lyria:', 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();
}