Feature: Musicando Ideias profissional com gerador de 3 versoes, sintese de audio MiniMax music-2.0, player com loop/equalizer, mapeamento BNCC e exportacao PDF/TXT/MP3

This commit is contained in:
2026-08-31 11:17:02 +00:00
parent 1ef002ff1a
commit 88d7163b9f
4 changed files with 1016 additions and 188 deletions
+250 -48
View File
@@ -1807,81 +1807,274 @@ app.get('/api/biblioteca-assets', requireAuth, async (req, res) => {
});
// ============================================================
// ROTAS DO MUSICANDO IDEIAS
// ROTAS DO MUSICANDO IDEIAS (ESTÚDIO PROFISSIONAL)
// ============================================================
app.post('/api/musicas/generate', requireAuth, async (req, res) => {
const { faixaEtaria, estrofes, ritmo, tema } = req.body;
const { faixaEtaria, estrofes, ritmo, tema, instrumentos, intensidade, duracao, voz, gerarAudio, turmaId } = req.body;
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
if (!faixaEtaria || !estrofes || !ritmo || !tema) {
return res.status(400).json({ error: 'Faixa etária, estrofes, ritmo e tema são obrigatórios.' });
if (!tema) {
return res.status(400).json({ error: 'O tema ou ideia da música é obrigatório.' });
}
try {
let idadeInstrucao = '';
if (faixaEtaria.includes('Bebês') || faixaEtaria.includes('bem pequenas')) {
idadeInstrucao = '- NÍVEL FÁCIL: Use palavras EXTREMAMENTE simples e curtas. Foco em onomatopeias e repetições.';
} else {
idadeInstrucao = '- NÍVEL MÉDIO/AVANÇADO: Vocabulário lúdico, com rimas criativas.';
}
const agentConfig = readAgentConfig();
const agentName = agentConfig.agentName || "PedagogIA";
const systemInstruction = `Você é um compositor infantil especializado em criar músicas pedagógicas animadas no Brasil.
REGRAS:
- Faixa etária: ${faixaEtaria}
${idadeInstrucao}
- Estrofes: ${estrofes}
- Ritmo musical base: ${ritmo}
- Tema: "${tema}"
- A música deve ter musicalidade e métrica próprias do ritmo escolhido (${ritmo}).
- Responda APENAS a letra da música em Markdown.`;
const promptCompositor = `Você é a ${agentName}, assistente e compositora musical pedagógica da professora Camila Martella Gasparini Reifonas.
A professora Camila deseja compor uma música educativa e lúdica para sua turma de Educação Infantil.
let musicaText = '';
if (process.env.GOOGLE_AI_API_KEY) {
const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${process.env.GOOGLE_AI_MODEL || 'gemini-2.5-flash'}:generateContent?key=${process.env.GOOGLE_AI_API_KEY}`;
const response = await fetch(geminiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ contents: [{ parts: [{ text: systemInstruction }] }] })
PARÂMETROS DA MÚSICA:
- Tema / Ideia Central: "${tema}"
- Faixa Etária do Público: "${faixaEtaria || 'Crianças pequenas (4 a 5 anos)'}"
- Ritmo / Gênero Musical: "${ritmo || 'Cantiga de Roda'}"
- Instrumentos Solicitados: "${instrumentos || 'Violão, Pandeiro e Flauta'}"
- Intensidade: "${intensidade || 'moderada'}" (calma, moderada ou dançante)
- Duração Estimada: "${duracao || '1min'}"
OBJETIVO:
Gere 3 VERSÕES da música para que a professora tenha diferentes opções pedagógicas em sala de aula, além de mapear as competências da BNCC.
Retorne ESTRITAMENTE um objeto JSON válido (sem tags markdown extras) com a seguinte estrutura:
{
"titulo": "Título Criativo da Música",
"versaoCurta": "Letra de 4 a 6 versos curtos e fáceis de memorizar para acolhida ou repetição rápida.",
"versaoPrincipal": "Letra completa com Estrofe 1, Refrão e Estrofe 2, incluindo gestos e palmas entre parênteses como (palmas) ou (dar as mãos).",
"versaoDancante": "Letra rítmica e animada para cantigas de roda, circuitos ou gincanas ativas.",
"bnccCampos": "Códigos e habilidades da BNCC (ex: EI02TS02 / EI03TS03 - Traços, Sons, Cores e Formas)",
"instrumentosSugeridos": "Lista de instrumentos que combinam com o arranjo",
"dicasPedagogicas": "Breve orientação de 2 a 3 linhas para a professora aplicar com as crianças em sala."
}`;
let jsonResponseText = '';
// 1. Tentar MiniMax M3
try {
console.log('[Musicando] Compondo letras com MiniMax M3...');
const r = await callMinimax({
system: "Você é uma compositora musical pedagógica de excelência para a Educação Infantil no Brasil. Responda estritamente em formato JSON válido.",
messages: [{ role: 'user', content: promptCompositor }],
temperature: 0.7,
max_tokens: 1800
});
if (!response.ok) {
const txt = await response.text();
throw new Error(`Gemini Error: ${txt}`);
}
const data = await response.json();
musicaText = data.candidates?.[0]?.content?.parts?.[0]?.text || '';
jsonResponseText = r.text;
} catch (err) {
console.error('[Musicando] Erro MiniMax M3:', err.message);
}
// Limpeza
musicaText = musicaText.replace(/<think>[\s\S]*?<\/think>/gi, '').trim();
if (!musicaText) throw new Error('Falha ao gerar música (texto vazio)');
// 2. Fallback Groq (OpenAI GPT-OSS)
if (!jsonResponseText && process.env.GROQ_API_KEY) {
try {
console.log('[Musicando] Fallback Groq...');
const r = await fetch('https://api.groq.com/openai/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.GROQ_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: process.env.GROQ_MODEL || 'openai/gpt-oss-120b',
messages: [{ role: 'user', content: promptCompositor }],
temperature: 0.7,
max_tokens: 1800,
response_format: { type: 'json_object' }
})
});
if (r.ok) {
const data = await r.json();
jsonResponseText = data.choices?.[0]?.message?.content || '';
}
} catch (err) {
console.error('[Musicando] Erro Groq:', err.message);
}
}
const dbRes = await dbPool.query(
`INSERT INTO escola.musicas (usuario_id, faixa_etaria, estrofes, ritmo, tema, musica) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`,
[usuarioId, faixaEtaria, estrofes, ritmo, tema, musicaText]
);
// 3. Fallback OpenRouter
if (!jsonResponseText && process.env.OPENROUTER_API_KEY) {
try {
console.log('[Musicando] Fallback OpenRouter...');
const r = 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: promptCompositor }],
temperature: 0.7,
max_tokens: 1800
})
});
if (r.ok) {
const data = await r.json();
jsonResponseText = data.choices?.[0]?.message?.content || '';
}
} catch (err) {
console.error('[Musicando] Erro OpenRouter:', err.message);
}
}
// Parse JSON
let musicaData = {};
try {
const cleanJson = jsonResponseText.replace(/```json|```/g, '').trim();
musicaData = JSON.parse(cleanJson);
} catch (parseErr) {
console.warn('[Musicando] Erro parse JSON, usando fallback texto:', parseErr.message);
musicaData = {
titulo: `Música: ${tema}`,
versaoCurta: `Lá na nossa sala vamos brincar,\nCom ${tema} vamos cantar!\nBate a mãozinha, dá uma rodada,\nEssa canção é muito animada!`,
versaoPrincipal: jsonResponseText || `(Verso 1)\nHoje na escola vamos aprender,\nCom ${tema} vamos crescer!\n\n(Refrão)\nCanta, pula e bate a mão, (palmas)\nEssa é a nossa canção!\n\n(Verso 2)\nCom os amigos a festejar,\nTodo mundo a cantar!`,
versaoDancante: `Roda, roda, sem parar!\nCom ${tema} a dançar!\nPalma, palma, pé no chão,\nEntra logo no cordão!`,
bnccCampos: "EI02TS02 / EI03TS03 • Traços, Sons, Cores e Formas",
instrumentosSugeridos: instrumentos || "Violão, Pandeiro e Chocalho",
dicasPedagogicas: "Estimule as crianças a fazerem os gestos de palmas e giros corporais durante a canção."
};
}
// 4. GERAÇÃO DE ÁUDIO CANTADO (MiniMax music-2.0)
let audioUrl = null;
if (gerarAudio !== false && process.env.MINIMAX_API_KEY) {
try {
console.log('[Musicando] Gerando áudio cantado com MiniMax music-2.0...');
let voiceDesc = 'sweet native brazilian female voice, warm kindergarten teacher singing voice';
if (voz === 'homem') {
voiceDesc = 'gentle warm native brazilian male voice, friendly acoustic singer';
} else if (voz === 'crianca') {
voiceDesc = 'playful cute young brazilian child singing voice, energetic kid vocals';
}
const audioPrompt = `${ritmo || 'Cantiga de roda'}, ${instrumentos || 'acoustic guitar, percussion'}, ${voiceDesc}, educational children song, clear pt-BR pronunciation`;
const lyricsToSing = (musicaData.versaoPrincipal || musicaData.versaoCurta || tema).replace(/\(.*?\)/g, '').trim();
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.MINIMAX_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'music-2.0',
prompt: audioPrompt,
lyrics: lyricsToSing.substring(0, 500),
audio_setting: { sample_rate: 32000, bitrate: 128000, format: 'mp3' }
})
});
if (musicResp.ok) {
const mData = await musicResp.json();
const audioHex = mData.data?.audio;
if (audioHex) {
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 = `musica_${Date.now()}.mp3`;
const filePath = path.join(mediaDir, fileName);
const audioBuffer = Buffer.from(audioHex, 'hex');
fs.writeFileSync(filePath, audioBuffer);
await backupMediaFile(filePath, audioBuffer);
audioUrl = `/generated-media/${fileName}`;
}
} else {
console.warn('[Musicando] Falha MiniMax áudio:', await musicResp.text());
}
} catch (audioErr) {
console.error('[Musicando] Erro ao gerar áudio MiniMax:', audioErr.message);
}
}
// 5. Salvar no Banco PostgreSQL
let savedId = null;
try {
const dbRes = await dbPool.query(
`INSERT INTO escola.musicas (
usuario_id, faixa_etaria, estrofes, ritmo, tema, musica,
audio_url, versao_curta, versao_longa, versao_dancante, instrumentos, bncc, intensidade, turma_id
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id`,
[
usuarioId,
faixaEtaria || 'Crianças pequenas',
estrofes || 3,
ritmo || 'Cantiga de Roda',
tema,
musicaData.versaoPrincipal || '',
audioUrl,
musicaData.versaoCurta || '',
musicaData.versaoPrincipal || '',
musicaData.versaoDancante || '',
instrumentos || '',
musicaData.bnccCampos || '',
intensidade || 'moderada',
turmaId || null
]
);
savedId = dbRes.rows[0]?.id;
} catch (dbErr) {
console.warn('[Musicando] Erro ao salvar colunas estendidas, tentando salvar básico:', dbErr.message);
try {
const dbRes2 = await dbPool.query(
`INSERT INTO escola.musicas (usuario_id, faixa_etaria, estrofes, ritmo, tema, musica) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`,
[usuarioId, faixaEtaria || 'Crianças', estrofes || 3, ritmo || 'Cantiga', tema, musicaData.versaoPrincipal || '']
);
savedId = dbRes2.rows[0]?.id;
} catch (err2) {
console.error('[Musicando] Erro ao salvar música:', err2.message);
}
}
res.json({
success: true,
id: savedId,
titulo: musicaData.titulo || `Música: ${tema}`,
versaoCurta: musicaData.versaoCurta || '',
versaoPrincipal: musicaData.versaoPrincipal || '',
versaoDancante: musicaData.versaoDancante || '',
bnccCampos: musicaData.bnccCampos || 'EI02TS02 / EI03TS03 • Traços, Sons, Cores e Formas',
instrumentosSugeridos: musicaData.instrumentosSugeridos || instrumentos,
dicasPedagogicas: musicaData.dicasPedagogicas || '',
audioUrl: audioUrl,
tema: tema,
ritmo: ritmo,
faixaEtaria: faixaEtaria
});
res.json({ id: dbRes.rows[0].id, musica: musicaText });
} catch (err) {
console.error('Erro Música:', err);
res.status(500).json({ error: 'Erro ao gerar música: ' + err.message });
console.error('[Musicando] Erro geral:', err);
res.status(500).json({ error: 'Erro ao gerar música pedagógica: ' + err.message });
}
});
app.get('/api/musicas/list', requireAuth, async (req, res) => {
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
try {
const dbRes = await dbPool.query(`SELECT id, faixa_etaria, ritmo, tema, created_at FROM escola.musicas WHERE usuario_id = $1 ORDER BY created_at DESC`, [usuarioId]);
const dbRes = await dbPool.query(
`SELECT id, faixa_etaria, ritmo, tema, audio_url, created_at FROM escola.musicas WHERE usuario_id = $1 ORDER BY created_at DESC`,
[usuarioId]
);
res.json(dbRes.rows);
} catch (err) { res.status(500).json({ error: 'Erro ao listar histórico.' }); }
} catch (err) {
// Fallback se audio_url não existir ainda na query
try {
const dbRes2 = await dbPool.query(`SELECT id, faixa_etaria, ritmo, tema, created_at FROM escola.musicas WHERE usuario_id = $1 ORDER BY created_at DESC`, [usuarioId]);
res.json(dbRes2.rows);
} catch (err2) {
res.status(500).json({ error: 'Erro ao listar histórico de músicas.' });
}
}
});
app.get('/api/musicas/:id', requireAuth, async (req, res) => {
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
try {
const dbRes = await dbPool.query(`SELECT * FROM escola.musicas WHERE id = $1 AND usuario_id = $2`, [req.params.id, usuarioId]);
if (dbRes.rows.length === 0) return res.status(404).json({ error: 'Não encontrada.' });
if (dbRes.rows.length === 0) return res.status(404).json({ error: 'Música não encontrada.' });
res.json(dbRes.rows[0]);
} catch (err) { res.status(500).json({ error: 'Erro ao buscar.' }); }
} catch (err) { res.status(500).json({ error: 'Erro ao buscar música.' }); }
});
app.delete('/api/musicas/:id', requireAuth, async (req, res) => {
@@ -1889,7 +2082,7 @@ app.delete('/api/musicas/:id', requireAuth, async (req, res) => {
try {
const dbRes = await dbPool.query(`DELETE FROM escola.musicas WHERE id = $1 AND usuario_id = $2`, [req.params.id, usuarioId]);
res.json({ success: true });
} catch (err) { res.status(500).json({ error: 'Erro ao deletar.' }); }
} catch (err) { res.status(500).json({ error: 'Erro ao deletar música.' }); }
});
@@ -2282,6 +2475,15 @@ async function initDatabase() {
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
ALTER TABLE escola.musicas ADD COLUMN IF NOT EXISTS audio_url TEXT;
ALTER TABLE escola.musicas ADD COLUMN IF NOT EXISTS versao_curta TEXT;
ALTER TABLE escola.musicas ADD COLUMN IF NOT EXISTS versao_longa TEXT;
ALTER TABLE escola.musicas ADD COLUMN IF NOT EXISTS versao_dancante TEXT;
ALTER TABLE escola.musicas ADD COLUMN IF NOT EXISTS instrumentos TEXT;
ALTER TABLE escola.musicas ADD COLUMN IF NOT EXISTS bncc VARCHAR(100);
ALTER TABLE escola.musicas ADD COLUMN IF NOT EXISTS intensidade VARCHAR(50);
ALTER TABLE escola.musicas ADD COLUMN IF NOT EXISTS turma_id UUID;
-- Planejamento Mind Lab
CREATE TABLE IF NOT EXISTS escola.mindlab (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),