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:
+465
-79
@@ -6722,7 +6722,7 @@ window.toggleCustomAudio = (btn) => {
|
||||
};
|
||||
};
|
||||
// ============================================================
|
||||
// MUSICANDO IDEIAS
|
||||
// MUSICANDO IDEIAS (ESTÚDIO PROFISSIONAL)
|
||||
// ============================================================
|
||||
const barBtnMusica = document.getElementById('barBtnMusica');
|
||||
const musicaModal = document.getElementById('musicaModal');
|
||||
@@ -6731,123 +6731,445 @@ window.toggleCustomAudio = (btn) => {
|
||||
const musicaHistoryModal = document.getElementById('musicaHistoryModal');
|
||||
const btnCloseMusicaHistory = document.getElementById('btnCloseMusicaHistory');
|
||||
|
||||
const musicaTemplateSelect = document.getElementById('musicaTemplateSelect');
|
||||
const musicaTemaInput = document.getElementById('musicaTemaInput');
|
||||
const musicaTurmaSelect = document.getElementById('musicaTurmaSelect');
|
||||
const musicaFaixaEtaria = document.getElementById('musicaFaixaEtaria');
|
||||
const musicaRitmo = document.getElementById('musicaRitmo');
|
||||
const musicaTemaInput = document.getElementById('musicaTemaInput');
|
||||
const musicaIntensidade = document.getElementById('musicaIntensidade');
|
||||
const musicaVozSelect = document.getElementById('musicaVozSelect');
|
||||
const musicaDuracaoSelect = document.getElementById('musicaDuracaoSelect');
|
||||
const btnGenerateMusica = document.getElementById('btnGenerateMusica');
|
||||
|
||||
const musicaGenerateLoader = document.getElementById('musicaGenerateLoader');
|
||||
const musicaResultArea = document.getElementById('musicaResultArea');
|
||||
const musicaTextOutput = document.getElementById('musicaTextOutput');
|
||||
const btnCopyMusica = document.getElementById('btnCopyMusica');
|
||||
const btnDownloadMusicaPdf = document.getElementById('btnDownloadMusicaPdf');
|
||||
|
||||
let currentMusicaEstrofes = 3;
|
||||
let currentMusica = '';
|
||||
// Elementos do Lado Direito
|
||||
const musicaBnccBadge = document.getElementById('musicaBnccBadge');
|
||||
const musicaAudioPlayerBox = document.getElementById('musicaAudioPlayerBox');
|
||||
const musicaAudioPlayer = document.getElementById('musicaAudioPlayer');
|
||||
const musicaPlayerTitle = document.getElementById('musicaPlayerTitle');
|
||||
const btnToggleMusicLoop = document.getElementById('btnToggleMusicLoop');
|
||||
const btnDownloadMusicaAudio = document.getElementById('btnDownloadMusicaAudio');
|
||||
const musicaWaveformVisualizer = document.getElementById('musicaWaveformVisualizer');
|
||||
|
||||
document.querySelectorAll('.btn-musica-estrofes').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
document.querySelectorAll('.btn-musica-estrofes').forEach(b => b.classList.remove('active'));
|
||||
e.target.classList.add('active');
|
||||
currentMusicaEstrofes = parseInt(e.target.dataset.estrofes);
|
||||
});
|
||||
});
|
||||
const musicaVersionsTabs = document.getElementById('musicaVersionsTabs');
|
||||
const btnTabVersaoPrincipal = document.getElementById('btnTabVersaoPrincipal');
|
||||
const btnTabVersaoCurta = document.getElementById('btnTabVersaoCurta');
|
||||
const btnTabVersaoDancante = document.getElementById('btnTabVersaoDancante');
|
||||
|
||||
const musicaPlaceholderText = document.getElementById('musicaPlaceholderText');
|
||||
const musicaActiveContentBox = document.getElementById('musicaActiveContentBox');
|
||||
const musicaActiveTitle = document.getElementById('musicaActiveTitle');
|
||||
const musicaActiveBadge = document.getElementById('musicaActiveBadge');
|
||||
const musicaActiveLyrics = document.getElementById('musicaActiveLyrics');
|
||||
const musicaPedagogicalTipBox = document.getElementById('musicaPedagogicalTipBox');
|
||||
const musicaPedagogicalTipText = document.getElementById('musicaPedagogicalTipText');
|
||||
|
||||
const btnCopyActiveLyrics = document.getElementById('btnCopyActiveLyrics');
|
||||
const btnDownloadActiveTxt = document.getElementById('btnDownloadActiveTxt');
|
||||
const btnExportActivePdf = document.getElementById('btnExportActivePdf');
|
||||
|
||||
// TEMPLATES PRÉ-CONFIGURADOS DO MUSICANDO IDEIAS
|
||||
const MUSICA_TEMPLATES = {
|
||||
horta: {
|
||||
tema: "Plantar sementinhas na horta da escola, regar a terra com carinho, ver os brotinhos crescerem e colher vegetais coloridos e saudáveis.",
|
||||
faixa: "Crianças pequenas (4 anos a 5 anos e 11 meses)",
|
||||
ritmo: "Cantiga de Roda",
|
||||
intensidade: "moderada",
|
||||
voz: "mulher",
|
||||
duracao: "1min",
|
||||
instrumentos: ["Violão acústico", "Flauta doce", "Pandeiro e percussão", "Xilofone infantil"]
|
||||
},
|
||||
familia: {
|
||||
tema: "O amor e o carinho da família, os abraços acolhedores na chegada e na saída da escola, gratidão e respeito aos pais e avós.",
|
||||
faixa: "Crianças bem pequenas (1 ano e 7 meses a 3 anos e 11 meses)",
|
||||
ritmo: "Canção de Ninar",
|
||||
intensidade: "calma",
|
||||
voz: "mulher",
|
||||
duracao: "1min",
|
||||
instrumentos: ["Violão acústico", "Metalofone e sinos", "Flauta doce"]
|
||||
},
|
||||
higiene: {
|
||||
tema: "Lavar as mãozinhas com água e sabão antes do lanche, escovar os dentinhos após comer e cuidar do corpinho limpinho e cheiroso.",
|
||||
faixa: "Crianças pequenas (4 anos a 5 anos e 11 meses)",
|
||||
ritmo: "Pop Infantil Alegre",
|
||||
intensidade: "dancante",
|
||||
voz: "crianca",
|
||||
duracao: "1min",
|
||||
instrumentos: ["Teclado e sintetizador", "Pandeiro e percussão", "Palmas e coro de crianças"]
|
||||
},
|
||||
mindlab: {
|
||||
tema: "Pensar antes de jogar, esperar a sua vez com calma, cooperar com os colegas de equipe e comemorar as jogadas inteligentes.",
|
||||
faixa: "Crianças pequenas (4 anos a 5 anos e 11 meses)",
|
||||
ritmo: "Cantiga de Roda",
|
||||
intensidade: "moderada",
|
||||
voz: "homem",
|
||||
duracao: "1min",
|
||||
instrumentos: ["Violão acústico", "Pandeiro e percussão", "Xilofone infantil"]
|
||||
},
|
||||
verao: {
|
||||
tema: "O sol brilhando no céu, brincadeiras com água e areia no parque, beber água fresca e sentir a brisa da natureza.",
|
||||
faixa: "Crianças pequenas (4 anos a 5 anos e 11 meses)",
|
||||
ritmo: "Forrózinho Lúdico",
|
||||
intensidade: "dancante",
|
||||
voz: "mulher",
|
||||
duracao: "1min",
|
||||
instrumentos: ["Acordeon", "Pandeiro e percussão", "Violão acústico", "Palmas e coro de crianças"]
|
||||
},
|
||||
alfabeto: {
|
||||
tema: "As letrinhas do alfabeto dançando, descobrindo os sons das vogais A E I O U e escrevendo o próprio nome com alegria.",
|
||||
faixa: "Crianças pequenas (4 anos a 5 anos e 11 meses)",
|
||||
ritmo: "Marchinha Escolar",
|
||||
intensidade: "dancante",
|
||||
voz: "mulher",
|
||||
duracao: "1min",
|
||||
instrumentos: ["Pandeiro e percussão", "Metalofone e sinos", "Palmas e coro de crianças"]
|
||||
},
|
||||
emocoes: {
|
||||
tema: "Reconhecer quando estamos felizes, com medo, bravos ou tranquilos, respirar fundo e receber um abraço amigo na roda.",
|
||||
faixa: "Crianças bem pequenas (1 ano e 7 meses a 3 anos e 11 meses)",
|
||||
ritmo: "Canção de Ninar",
|
||||
intensidade: "calma",
|
||||
voz: "mulher",
|
||||
duracao: "1min",
|
||||
instrumentos: ["Violão acústico", "Flauta doce", "Metalofone e sinos"]
|
||||
}
|
||||
};
|
||||
|
||||
// Estado atual da música gerada
|
||||
let activeMusicaData = {
|
||||
titulo: '',
|
||||
versaoPrincipal: '',
|
||||
versaoCurta: '',
|
||||
versaoDancante: '',
|
||||
bnccCampos: '',
|
||||
instrumentos: '',
|
||||
dicas: '',
|
||||
audioUrl: '',
|
||||
tema: '',
|
||||
ritmo: '',
|
||||
faixaEtaria: ''
|
||||
};
|
||||
let activeMusicaTab = 'principal';
|
||||
let isLoopActive = false;
|
||||
|
||||
// Carregar turmas no seletor
|
||||
const loadTurmasForMusica = async () => {
|
||||
if (!musicaTurmaSelect) return;
|
||||
try {
|
||||
const res = await fetch('/api/turmas');
|
||||
if (res.ok) {
|
||||
const turmas = await res.json();
|
||||
musicaTurmaSelect.innerHTML = '<option value="">-- Selecionar Turma --</option>';
|
||||
turmas.forEach(t => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = t.id;
|
||||
opt.textContent = `${t.nome} (${t.ano || '2026'})`;
|
||||
opt.dataset.nome = t.nome;
|
||||
musicaTurmaSelect.appendChild(opt);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Erro ao carregar turmas para música:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Abrir Modal
|
||||
if (barBtnMusica) {
|
||||
barBtnMusica.addEventListener('click', () => {
|
||||
musicaModal.style.display = 'flex';
|
||||
musicaTemaInput.value = '';
|
||||
musicaResultArea.style.display = 'none';
|
||||
musicaGenerateLoader.style.display = 'none';
|
||||
btnGenerateMusica.disabled = false;
|
||||
currentMusica = '';
|
||||
loadTurmasForMusica();
|
||||
if (!musicaTemaInput.value) {
|
||||
applyMusicaTemplate('horta');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (btnCloseMusicaModal) musicaModal.addEventListener('click', (e) => { if(e.target === musicaModal) musicaModal.style.display = 'none'; });
|
||||
if (btnCloseMusicaModal) btnCloseMusicaModal.addEventListener('click', () => musicaModal.style.display = 'none');
|
||||
|
||||
if (btnOpenMusicaHistory) {
|
||||
btnOpenMusicaHistory.addEventListener('click', () => {
|
||||
musicaHistoryModal.style.display = 'flex';
|
||||
loadMusicaHistory();
|
||||
if (btnCloseMusicaModal) {
|
||||
btnCloseMusicaModal.addEventListener('click', () => {
|
||||
musicaModal.style.display = 'none';
|
||||
if (musicaAudioPlayer) musicaAudioPlayer.pause();
|
||||
});
|
||||
}
|
||||
if (btnCloseMusicaHistory) btnCloseMusicaHistory.addEventListener('click', () => musicaHistoryModal.style.display = 'none');
|
||||
musicaHistoryModal.addEventListener('click', (e) => { if(e.target === musicaHistoryModal) musicaHistoryModal.style.display = 'none'; });
|
||||
|
||||
musicaModal.addEventListener('click', (e) => {
|
||||
if (e.target === musicaModal) {
|
||||
musicaModal.style.display = 'none';
|
||||
if (musicaAudioPlayer) musicaAudioPlayer.pause();
|
||||
}
|
||||
});
|
||||
|
||||
// Aplicar Template
|
||||
const applyMusicaTemplate = (key) => {
|
||||
const tmpl = MUSICA_TEMPLATES[key];
|
||||
if (!tmpl) return;
|
||||
|
||||
if (musicaTemaInput) musicaTemaInput.value = tmpl.tema;
|
||||
if (musicaFaixaEtaria) musicaFaixaEtaria.value = tmpl.faixa;
|
||||
if (musicaRitmo) musicaRitmo.value = tmpl.ritmo;
|
||||
if (musicaIntensidade) musicaIntensidade.value = tmpl.intensidade;
|
||||
if (musicaVozSelect) musicaVozSelect.value = tmpl.voz;
|
||||
if (musicaDuracaoSelect) musicaDuracaoSelect.value = tmpl.duracao;
|
||||
|
||||
// Ajustar checkboxes de instrumentos
|
||||
const checkboxes = document.querySelectorAll('#musicaInstrumentsContainer input[type="checkbox"]');
|
||||
checkboxes.forEach(cb => {
|
||||
cb.checked = tmpl.instrumentos.includes(cb.value);
|
||||
});
|
||||
};
|
||||
|
||||
if (musicaTemplateSelect) {
|
||||
musicaTemplateSelect.addEventListener('change', (e) => {
|
||||
if (e.target.value) {
|
||||
applyMusicaTemplate(e.target.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Alternar Versões no Painel Direito
|
||||
const switchMusicaVersionTab = (tab) => {
|
||||
activeMusicaTab = tab;
|
||||
[btnTabVersaoPrincipal, btnTabVersaoCurta, btnTabVersaoDancante].forEach(b => {
|
||||
if (b) {
|
||||
b.classList.remove('active');
|
||||
b.style.background = 'transparent';
|
||||
b.style.color = 'var(--text-secondary)';
|
||||
}
|
||||
});
|
||||
|
||||
let currentLyrics = '';
|
||||
let badgeText = '';
|
||||
|
||||
if (tab === 'principal') {
|
||||
if (btnTabVersaoPrincipal) {
|
||||
btnTabVersaoPrincipal.classList.add('active');
|
||||
btnTabVersaoPrincipal.style.background = 'rgba(168,85,247,0.15)';
|
||||
btnTabVersaoPrincipal.style.color = '#c084fc';
|
||||
}
|
||||
currentLyrics = activeMusicaData.versaoPrincipal;
|
||||
badgeText = '🌟 Versão Completa (com gestos)';
|
||||
} else if (tab === 'curta') {
|
||||
if (btnTabVersaoCurta) {
|
||||
btnTabVersaoCurta.classList.add('active');
|
||||
btnTabVersaoCurta.style.background = 'rgba(168,85,247,0.15)';
|
||||
btnTabVersaoCurta.style.color = '#c084fc';
|
||||
}
|
||||
currentLyrics = activeMusicaData.versaoCurta;
|
||||
badgeText = '⚡ Rascunho / Memorização Rápida';
|
||||
} else if (tab === 'dancante') {
|
||||
if (btnTabVersaoDancante) {
|
||||
btnTabVersaoDancante.classList.add('active');
|
||||
btnTabVersaoDancante.style.background = 'rgba(168,85,247,0.15)';
|
||||
btnTabVersaoDancante.style.color = '#c084fc';
|
||||
}
|
||||
currentLyrics = activeMusicaData.versaoDancante;
|
||||
badgeText = '🎉 Versão Dançante / Roda';
|
||||
}
|
||||
|
||||
if (musicaActiveBadge) musicaActiveBadge.textContent = badgeText;
|
||||
if (musicaActiveLyrics) musicaActiveLyrics.textContent = currentLyrics || 'Nenhuma letra para esta versão.';
|
||||
};
|
||||
|
||||
if (btnTabVersaoPrincipal) btnTabVersaoPrincipal.addEventListener('click', () => switchMusicaVersionTab('principal'));
|
||||
if (btnTabVersaoCurta) btnTabVersaoCurta.addEventListener('click', () => switchMusicaVersionTab('curta'));
|
||||
if (btnTabVersaoDancante) btnTabVersaoDancante.addEventListener('click', () => switchMusicaVersionTab('dancante'));
|
||||
|
||||
// Controle de Loop de Áudio
|
||||
if (btnToggleMusicLoop) {
|
||||
btnToggleMusicLoop.addEventListener('click', () => {
|
||||
isLoopActive = !isLoopActive;
|
||||
if (musicaAudioPlayer) musicaAudioPlayer.loop = isLoopActive;
|
||||
btnToggleMusicLoop.textContent = isLoopActive ? '🔁 Loop: LIGADO' : '🔁 Loop: Desligado';
|
||||
btnToggleMusicLoop.style.background = isLoopActive ? 'rgba(16, 185, 129, 0.2)' : 'rgba(255,255,255,0.05)';
|
||||
btnToggleMusicLoop.style.color = isLoopActive ? '#34d399' : 'var(--text-secondary)';
|
||||
});
|
||||
}
|
||||
|
||||
// Equalizer / Waveform Animation quando toca
|
||||
if (musicaAudioPlayer) {
|
||||
musicaAudioPlayer.addEventListener('play', () => {
|
||||
const bars = document.querySelectorAll('#musicaWaveformVisualizer .wave-bar');
|
||||
bars.forEach(b => b.classList.add('wave-active'));
|
||||
});
|
||||
musicaAudioPlayer.addEventListener('pause', () => {
|
||||
const bars = document.querySelectorAll('#musicaWaveformVisualizer .wave-bar');
|
||||
bars.forEach(b => b.classList.remove('wave-active'));
|
||||
});
|
||||
}
|
||||
|
||||
// --- GERAR MÚSICA (COMPOR 3 VERSÕES + ÁUDIO) ---
|
||||
if (btnGenerateMusica) {
|
||||
btnGenerateMusica.addEventListener('click', async () => {
|
||||
const tema = musicaTemaInput.value.trim();
|
||||
const tema = (musicaTemaInput?.value || '').trim();
|
||||
if (!tema) {
|
||||
showToast('Por favor, informe o tema da música.', 'error');
|
||||
await showCustomAlert('Aviso', 'Por favor, informe a ideia ou tema da música.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Coletar instrumentos selecionados
|
||||
const selectedInstruments = Array.from(
|
||||
document.querySelectorAll('#musicaInstrumentsContainer input[type="checkbox"]:checked')
|
||||
).map(cb => cb.value).join(', ');
|
||||
|
||||
btnGenerateMusica.disabled = true;
|
||||
musicaResultArea.style.display = 'none';
|
||||
musicaGenerateLoader.style.display = 'flex';
|
||||
if (musicaGenerateLoader) musicaGenerateLoader.style.display = 'flex';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/musicas/generate', {
|
||||
const res = await fetch('/api/musicas/generate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
faixaEtaria: musicaFaixaEtaria.value,
|
||||
estrofes: currentMusicaEstrofes,
|
||||
ritmo: musicaRitmo.value,
|
||||
tema: tema
|
||||
tema,
|
||||
faixaEtaria: musicaFaixaEtaria?.value,
|
||||
ritmo: musicaRitmo?.value,
|
||||
instrumentos: selectedInstruments || 'Violão acústico, Pandeiro e Flauta',
|
||||
intensidade: musicaIntensidade?.value || 'moderada',
|
||||
duracao: musicaDuracaoSelect?.value || '1min',
|
||||
voz: musicaVozSelect?.value || 'mulher',
|
||||
turmaId: musicaTurmaSelect?.value || null,
|
||||
gerarAudio: true
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
currentMusica = data.musica;
|
||||
musicaTextOutput.innerHTML = window.marked ? marked.parse(currentMusica) : currentMusica;
|
||||
musicaResultArea.style.display = 'flex';
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok && data.success) {
|
||||
activeMusicaData = {
|
||||
titulo: data.titulo || `Música: ${tema}`,
|
||||
versaoPrincipal: data.versaoPrincipal,
|
||||
versaoCurta: data.versaoCurta,
|
||||
versaoDancante: data.versaoDancante,
|
||||
bnccCampos: data.bnccCampos,
|
||||
instrumentos: data.instrumentosSugeridos || selectedInstruments,
|
||||
dicas: data.dicasPedagogicas,
|
||||
audioUrl: data.audioUrl,
|
||||
tema: tema,
|
||||
ritmo: data.ritmo || musicaRitmo?.value,
|
||||
faixaEtaria: data.faixaEtaria || musicaFaixaEtaria?.value
|
||||
};
|
||||
|
||||
// Atualizar UI
|
||||
if (musicaPlaceholderText) musicaPlaceholderText.style.display = 'none';
|
||||
if (musicaVersionsTabs) musicaVersionsTabs.style.display = 'flex';
|
||||
if (musicaActiveContentBox) musicaActiveContentBox.style.display = 'flex';
|
||||
|
||||
if (musicaActiveTitle) musicaActiveTitle.textContent = activeMusicaData.titulo;
|
||||
if (musicaBnccBadge) {
|
||||
musicaBnccBadge.textContent = `BNCC: ${activeMusicaData.bnccCampos.split('•')[0].trim()}`;
|
||||
musicaBnccBadge.style.display = 'inline-block';
|
||||
}
|
||||
|
||||
if (musicaPedagogicalTipText && activeMusicaData.dicas) {
|
||||
musicaPedagogicalTipText.textContent = activeMusicaData.dicas;
|
||||
musicaPedagogicalTipBox.style.display = 'block';
|
||||
}
|
||||
|
||||
// Se gerou áudio com MiniMax
|
||||
if (data.audioUrl) {
|
||||
musicaAudioPlayer.src = data.audioUrl;
|
||||
if (musicaPlayerTitle) musicaPlayerTitle.textContent = activeMusicaData.titulo;
|
||||
if (btnDownloadMusicaAudio) btnDownloadMusicaAudio.href = data.audioUrl;
|
||||
if (musicaAudioPlayerBox) musicaAudioPlayerBox.style.display = 'flex';
|
||||
} else {
|
||||
if (musicaAudioPlayerBox) musicaAudioPlayerBox.style.display = 'none';
|
||||
}
|
||||
|
||||
switchMusicaVersionTab('principal');
|
||||
showToast('3 versões de música pedagógica compostas com sucesso!', 'success');
|
||||
|
||||
} else {
|
||||
showToast(data.error || 'Erro ao gerar música.', 'error');
|
||||
await showCustomAlert('Erro', data.error || 'Erro ao compor música.');
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
showToast('Erro de conexão.', 'error');
|
||||
console.error('Erro ao compor música:', err);
|
||||
await showCustomAlert('Erro', 'Não foi possível conectar ao servidor: ' + err.message);
|
||||
} finally {
|
||||
musicaGenerateLoader.style.display = 'none';
|
||||
btnGenerateMusica.disabled = false;
|
||||
if (musicaGenerateLoader) musicaGenerateLoader.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (btnCopyMusica) {
|
||||
btnCopyMusica.addEventListener('click', () => {
|
||||
if (currentMusica) {
|
||||
navigator.clipboard.writeText(currentMusica);
|
||||
showToast('Música copiada para a área de transferência!', 'success');
|
||||
// Copiar Letra Ativa
|
||||
if (btnCopyActiveLyrics) {
|
||||
btnCopyActiveLyrics.addEventListener('click', () => {
|
||||
const text = musicaActiveLyrics?.textContent || '';
|
||||
if (text) {
|
||||
navigator.clipboard.writeText(`${activeMusicaData.titulo}\n\n${text}`);
|
||||
showToast('Letra copiada para a área de transferência!', 'success');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (btnDownloadMusicaPdf) {
|
||||
btnDownloadMusicaPdf.addEventListener('click', () => {
|
||||
if (!currentMusica) return;
|
||||
// Baixar Letra TXT
|
||||
if (btnDownloadActiveTxt) {
|
||||
btnDownloadActiveTxt.addEventListener('click', () => {
|
||||
const text = musicaActiveLyrics?.textContent || '';
|
||||
if (!text) return;
|
||||
const content = `MÚSICA PEDAGÓGICA - PEDAGOGIA\n========================================\nTítulo: ${activeMusicaData.titulo}\nFaixa Etária: ${activeMusicaData.faixaEtaria}\nRitmo: ${activeMusicaData.ritmo}\nBNCC: ${activeMusicaData.bnccCampos}\nInstrumentos: ${activeMusicaData.instrumentos}\n========================================\n\n${text}\n\n----------------------------------------\nOrientação Pedagógica:\n${activeMusicaData.dicas}\n`;
|
||||
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = `musica_${(activeMusicaData.titulo || 'pedagogica').replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase()}.txt`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
});
|
||||
}
|
||||
|
||||
// Exportar PDF Formatado
|
||||
if (btnExportActivePdf) {
|
||||
btnExportActivePdf.addEventListener('click', () => {
|
||||
const lyrics = musicaActiveLyrics?.textContent || '';
|
||||
if (!lyrics) return;
|
||||
|
||||
const printWindow = window.open('', '_blank');
|
||||
printWindow.document.write(`
|
||||
<html>
|
||||
<head>
|
||||
<title>Música - Assistente PedaGog</title>
|
||||
<title>${activeMusicaData.titulo} - PedaGog</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600;700&family=Outfit:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Outfit', 'Inter', sans-serif; padding: 40px; color: #333; line-height: 1.6; }
|
||||
h1, h2, h3 { color: #10b981; margin-top: 24px; margin-bottom: 12px; }
|
||||
@media print { body { padding: 0; } button { display: none; } }
|
||||
body { font-family: 'Fredoka', 'Outfit', sans-serif; padding: 40px; color: #1e293b; background: #fffdfa; line-height: 1.7; }
|
||||
.header-box { border-bottom: 3px dashed #a855f7; padding-bottom: 16px; margin-bottom: 24px; }
|
||||
h1 { color: #7c3aed; margin: 0 0 8px 0; font-size: 2rem; }
|
||||
.meta-tags { display: flex; gap: 12px; font-size: 0.9rem; color: #64748b; font-weight: 600; margin-bottom: 8px; flex-wrap: wrap; }
|
||||
.meta-tag { background: #f3e8ff; color: #7e22ce; padding: 4px 10px; border-radius: 6px; }
|
||||
.lyrics-box { font-size: 1.15rem; background: #f8fafc; padding: 24px; border-radius: 12px; border: 1px solid #e2e8f0; white-space: pre-wrap; margin-bottom: 24px; box-shadow: 0 2px 4px rgba(0,0,0,0.02); }
|
||||
.tip-box { background: #f0fdf4; border: 1px solid #bbf7d0; color: #166534; padding: 14px 18px; border-radius: 8px; font-size: 0.92rem; }
|
||||
@media print { body { padding: 20px; } button { display: none; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div style="max-width: 800px; margin: 0 auto;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 2px solid #e2e8f0; padding-bottom: 10px; margin-bottom: 20px;">
|
||||
<h1 style="margin: 0;">🎵 Música Pedagógica</h1>
|
||||
<button onclick="window.print()" style="background: #10b981; color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-weight: bold;">Imprimir / PDF</button>
|
||||
<div style="max-width: 750px; margin: 0 auto;">
|
||||
<div class="header-box">
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
|
||||
<div>
|
||||
<h1>🎵 ${activeMusicaData.titulo}</h1>
|
||||
<div class="meta-tags">
|
||||
<span class="meta-tag">👶 ${activeMusicaData.faixaEtaria}</span>
|
||||
<span class="meta-tag">🥁 ${activeMusicaData.ritmo}</span>
|
||||
<span class="meta-tag">📚 ${activeMusicaData.bnccCampos.split('•')[0]}</span>
|
||||
</div>
|
||||
<div style="font-size: 0.85rem; color: #64748b;">🎸 Instrumentos: ${activeMusicaData.instrumentos}</div>
|
||||
</div>
|
||||
<button onclick="window.print()" style="background: #a855f7; color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-weight: bold; font-family: sans-serif;">🖨️ Imprimir / Salvar PDF</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lyrics-box">${lyrics}</div>
|
||||
|
||||
${activeMusicaData.dicas ? `
|
||||
<div class="tip-box">
|
||||
💡 <strong>Vivência Pedagógica Sugerida:</strong><br>
|
||||
${activeMusicaData.dicas}
|
||||
</div>` : ''}
|
||||
|
||||
<div style="margin-top: 30px; text-align: center; font-size: 0.8rem; color: #94a3b8; border-top: 1px solid #e2e8f0; padding-top: 10px;">
|
||||
Gerado por PedagogIA • Assistente Pedagógica da Professora Camila
|
||||
</div>
|
||||
<div>${window.marked ? marked.parse(currentMusica) : currentMusica}</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -6856,10 +7178,31 @@ window.toggleCustomAudio = (btn) => {
|
||||
});
|
||||
}
|
||||
|
||||
// --- HISTÓRICO DE MÚSICAS ---
|
||||
if (btnOpenMusicaHistory) {
|
||||
btnOpenMusicaHistory.addEventListener('click', () => {
|
||||
musicaHistoryModal.style.display = 'flex';
|
||||
loadMusicaHistory();
|
||||
});
|
||||
}
|
||||
|
||||
if (btnCloseMusicaHistory) {
|
||||
btnCloseMusicaHistory.addEventListener('click', () => {
|
||||
musicaHistoryModal.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
musicaHistoryModal.addEventListener('click', (e) => {
|
||||
if (e.target === musicaHistoryModal) {
|
||||
musicaHistoryModal.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
async function loadMusicaHistory() {
|
||||
const listContainer = document.getElementById('musicaHistoryList');
|
||||
if (!listContainer) return;
|
||||
listContainer.innerHTML = '<p style="text-align:center; color:var(--text-secondary);">Carregando...</p>';
|
||||
listContainer.innerHTML = '<p style="text-align:center; color:var(--text-secondary);">Carregando histórico...</p>';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/musicas/list');
|
||||
const data = await response.json();
|
||||
@@ -6869,43 +7212,86 @@ window.toggleCustomAudio = (btn) => {
|
||||
const date = new Date(item.created_at).toLocaleString('pt-BR');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'history-item-card';
|
||||
div.style.cssText = 'background: rgba(255,255,255,0.02); padding: 14px; border-radius: 8px; border: 1px solid var(--border-light); display: flex; flex-direction: column; gap: 8px; cursor: pointer; position: relative;';
|
||||
div.style.cssText = 'background: rgba(255,255,255,0.03); padding: 14px; border-radius: 10px; border: 1px solid var(--border-light); display: flex; flex-direction: column; gap: 8px; cursor: pointer; position: relative; transition: all 0.2s;';
|
||||
div.innerHTML = `
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; padding-right: 30px;">
|
||||
<strong style="color: var(--brand-green); font-size: 0.95rem;">${item.faixa_etaria} - ${item.ritmo}</strong>
|
||||
<span style="font-size: 0.75rem; color: var(--text-secondary);">${date}</span>
|
||||
<strong style="color: #c084fc; font-size: 0.95rem;">${item.faixa_etaria} • ${item.ritmo}</strong>
|
||||
<span style="font-size: 0.72rem; color: var(--text-secondary);">${date}</span>
|
||||
</div>
|
||||
<p style="margin: 0; font-size: 0.85rem; color: var(--text-primary); opacity: 0.9;">Tema: ${item.tema}</p>
|
||||
<p style="margin: 0; font-size: 0.85rem; color: var(--text-primary); opacity: 0.9;"><strong>Tema:</strong> ${item.tema}</p>
|
||||
${item.audio_url ? `
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-top: 4px;">
|
||||
<span style="font-size: 0.75rem; color: #10b981; font-weight: 600;">🎧 Áudio Disponível</span>
|
||||
</div>
|
||||
` : ''}
|
||||
<button class="btn-delete-musica" data-id="${item.id}" style="position: absolute; top: 12px; right: 12px; background: transparent; border: none; color: #ef4444; cursor: pointer; font-size: 1.1rem; padding: 4px; border-radius: 4px;" title="Excluir">🗑️</button>
|
||||
`;
|
||||
|
||||
div.addEventListener('click', async (e) => {
|
||||
if (e.target.closest('.btn-delete-musica')) {
|
||||
e.stopPropagation();
|
||||
if (confirm('Tem certeza que deseja excluir?')) {
|
||||
if (confirm('Tem certeza que deseja excluir esta música do histórico?')) {
|
||||
try {
|
||||
const delRes = await fetch(`/api/musicas/${item.id}`, { method: 'DELETE' });
|
||||
if (delRes.ok) { showToast('Música excluída.', 'success'); loadMusicaHistory(); }
|
||||
} catch (err) { showToast('Erro ao excluir.', 'error'); }
|
||||
if (delRes.ok) {
|
||||
showToast('Música excluída do histórico.', 'success');
|
||||
loadMusicaHistory();
|
||||
}
|
||||
} catch (err) {
|
||||
showToast('Erro ao excluir música.', 'error');
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Carregar música clicada
|
||||
try {
|
||||
const res = await fetch(`/api/musicas/${item.id}`);
|
||||
const detail = await res.json();
|
||||
if (res.ok) {
|
||||
currentMusica = detail.musica;
|
||||
musicaTextOutput.innerHTML = window.marked ? marked.parse(currentMusica) : currentMusica;
|
||||
musicaResultArea.style.display = 'flex';
|
||||
activeMusicaData = {
|
||||
titulo: `Música: ${detail.tema}`,
|
||||
versaoPrincipal: detail.versao_longa || detail.musica,
|
||||
versaoCurta: detail.versao_curta || detail.musica,
|
||||
versaoDancante: detail.versao_dancante || detail.musica,
|
||||
bnccCampos: detail.bncc || 'EI02TS02 • Traços, Sons, Cores e Formas',
|
||||
instrumentos: detail.instrumentos || 'Violão e Percussão',
|
||||
dicas: '',
|
||||
audioUrl: detail.audio_url,
|
||||
tema: detail.tema,
|
||||
ritmo: detail.ritmo,
|
||||
faixaEtaria: detail.faixa_etaria
|
||||
};
|
||||
|
||||
if (musicaPlaceholderText) musicaPlaceholderText.style.display = 'none';
|
||||
if (musicaVersionsTabs) musicaVersionsTabs.style.display = 'flex';
|
||||
if (musicaActiveContentBox) musicaActiveContentBox.style.display = 'flex';
|
||||
|
||||
if (musicaActiveTitle) musicaActiveTitle.textContent = activeMusicaData.titulo;
|
||||
if (detail.audio_url) {
|
||||
musicaAudioPlayer.src = detail.audio_url;
|
||||
if (btnDownloadMusicaAudio) btnDownloadMusicaAudio.href = detail.audio_url;
|
||||
if (musicaAudioPlayerBox) musicaAudioPlayerBox.style.display = 'flex';
|
||||
} else {
|
||||
if (musicaAudioPlayerBox) musicaAudioPlayerBox.style.display = 'none';
|
||||
}
|
||||
|
||||
switchMusicaVersionTab('principal');
|
||||
musicaHistoryModal.style.display = 'none';
|
||||
}
|
||||
} catch (err) { showToast('Erro ao carregar.', 'error'); }
|
||||
} catch (err) {
|
||||
showToast('Erro ao carregar detalhes da música.', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
listContainer.appendChild(div);
|
||||
});
|
||||
} else {
|
||||
listContainer.innerHTML = '<p style="text-align:center; color:var(--text-secondary);">Nenhum histórico.</p>';
|
||||
listContainer.innerHTML = '<p style="text-align:center; color:var(--text-secondary); padding: 20px;">Nenhuma música gerada até o momento.</p>';
|
||||
}
|
||||
} catch (err) { listContainer.innerHTML = '<p style="text-align:center; color:#ef4444;">Erro ao carregar.</p>'; }
|
||||
} catch (err) {
|
||||
listContainer.innerHTML = '<p style="text-align:center; color:#ef4444;">Erro ao carregar histórico de músicas.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+228
-61
@@ -958,85 +958,252 @@
|
||||
|
||||
<!-- Modal: Estúdio de Poesia -->
|
||||
<!-- ========================================== -->
|
||||
<!-- MODAL: MUSICANDO IDEIAS -->
|
||||
<!-- MODAL: MUSICANDO IDEIAS (ESTÚDIO PROFISSIONAL) -->
|
||||
<!-- ========================================== -->
|
||||
<div id="musicaModal" class="settings-modal" style="display: none;">
|
||||
<div class="settings-modal-content" style="max-width: 600px; width: 95%; max-height: 90vh; display: flex; flex-direction: column;">
|
||||
<div class="settings-modal-header" style="background: var(--bg-secondary); border-bottom: 1px solid var(--border-light);">
|
||||
<h3 style="display: flex; align-items: center; gap: 8px; font-family: 'Outfit', sans-serif;">🎵 Musicando Ideias PedaGog</h3>
|
||||
<button id="btnCloseMusicaModal" class="btn-close-modal" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.5rem;">×</button>
|
||||
</div>
|
||||
<div class="settings-modal-content" style="max-width: 1050px; width: 95%; max-height: 92vh; display: flex; flex-direction: column;">
|
||||
|
||||
<div style="padding: 12px 20px; display: flex; justify-content: flex-end; border-bottom: 1px solid var(--border-light);">
|
||||
<button id="btnOpenMusicaHistory" style="background: transparent; border: 1px solid var(--border-light); color: var(--text-primary); padding: 6px 12px; border-radius: 6px; cursor: pointer; display: flex; align-items: center; gap: 6px; font-size: 0.85rem;">🕒 Ver Histórico</button>
|
||||
<!-- Modal Header -->
|
||||
<div class="settings-modal-header" style="background: var(--bg-secondary); border-bottom: 1px solid var(--border-light); padding: 14px 20px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;">
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<span style="font-size: 1.4rem;">🎵</span>
|
||||
<div>
|
||||
<h3 style="margin: 0; font-family: 'Outfit', sans-serif; font-size: 1.15rem; color: var(--text-primary);">Musicando Ideias • Estúdio PedagogIA</h3>
|
||||
<span style="font-size: 0.75rem; color: var(--text-secondary);">Composição de cantigas personalizadas, 3 versões pedagógicas e áudio cantado</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px; align-items: center;">
|
||||
<button type="button" id="btnOpenMusicaHistory" style="background: rgba(168, 85, 247, 0.1); border: 1px solid rgba(168, 85, 247, 0.3); color: #c084fc; padding: 6px 12px; border-radius: 6px; cursor: pointer; display: flex; align-items: center; gap: 6px; font-size: 0.82rem; font-weight: 600;">🕒 Histórico</button>
|
||||
<button type="button" id="btnCloseMusicaModal" class="btn-close-modal" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.5rem; line-height: 1;">×</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-modal-body" style="padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 18px; flex: 1;">
|
||||
<!-- Modal Body com 2 Colunas -->
|
||||
<div class="settings-modal-body" style="padding: 20px; overflow-y: auto; display: flex; flex-direction: row; gap: 24px; flex: 1; flex-wrap: wrap;">
|
||||
|
||||
<div class="settings-group">
|
||||
<label style="color: var(--text-secondary); font-size: 0.82rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">1. Faixa Etária (Idade da Criança)</label>
|
||||
<select id="musicaFaixaEtaria" class="obs-select" style="width: 100%; margin-top: 4px;">
|
||||
<option value="Bebês (0 a 1 ano e 6 meses)">Bebês (0 a 1 ano e 6 meses)</option>
|
||||
<option value="Crianças bem pequenas (1 ano e 7 meses a 3 anos e 11 meses)">Crianças bem pequenas (1 ano e 7 meses a 3 anos e 11 meses)</option>
|
||||
<option value="Crianças pequenas (4 anos a 5 anos e 11 meses)" selected>Crianças pequenas (4 anos a 5 anos e 11 meses)</option>
|
||||
<option value="Ensino Fundamental I (6 a 10 anos)">Ensino Fundamental I (6 a 10 anos)</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Lado Esquerdo: Parâmetros e Ideia -->
|
||||
<div id="musicaLeftPanel" style="flex: 1; min-width: 320px; display: flex; flex-direction: column; gap: 14px;">
|
||||
|
||||
<h4 style="margin: 0; color: var(--text-primary); border-bottom: 1px dashed var(--border-light); padding-bottom: 6px; font-size: 0.92rem;">📝 1. Descrição da Ideia Musical</h4>
|
||||
|
||||
<div class="settings-group">
|
||||
<label style="color: var(--text-secondary); font-size: 0.82rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">2. Ritmo Musical</label>
|
||||
<select id="musicaRitmo" class="obs-select" style="width: 100%; margin-top: 4px;">
|
||||
<option value="Cantiga de Roda">Cantiga de Roda</option>
|
||||
<option value="Pop Infantil">Pop Infantil</option>
|
||||
<option value="Samba/Pagode">Samba/Pagode</option>
|
||||
<option value="Forró">Forró</option>
|
||||
<option value="Rock">Rock</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<label style="color: var(--text-secondary); font-size: 0.82rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">3. Quantidade de Estrofes</label>
|
||||
<div class="music-option-grid" style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-top: 4px;">
|
||||
<button type="button" class="btn-music-option btn-musica-estrofes" data-estrofes="2">2 Estrofes</button>
|
||||
<button type="button" class="btn-music-option btn-musica-estrofes active" data-estrofes="3">3 Estrofes</button>
|
||||
<button type="button" class="btn-music-option btn-musica-estrofes" data-estrofes="4">4 Estrofes</button>
|
||||
<button type="button" class="btn-music-option btn-musica-estrofes" data-estrofes="5">5 Estrofes</button>
|
||||
<!-- TEMPLATES RÁPIDOS -->
|
||||
<div class="settings-group">
|
||||
<label style="color: var(--text-secondary); font-size: 0.75rem; font-weight: 600; text-transform: uppercase;">Modelos de Ideias (Templates)</label>
|
||||
<select id="musicaTemplateSelect" class="obs-select" style="width: 100%; margin-top: 4px; color: #a855f7; font-weight: 600;">
|
||||
<option value="">✨ Escolha um modelo temático...</option>
|
||||
<option value="horta">🌱 Horta Escolar e Alimentação Consciente</option>
|
||||
<option value="familia">👨👩👧👦 Dia da Família e Vínculos de Afeto</option>
|
||||
<option value="higiene">🧼 Higiene das Mãos e Autocuidado</option>
|
||||
<option value="mindlab">🧩 Jogos, Cooperação e Raciocínio (Mind Lab)</option>
|
||||
<option value="verao">🌞 Projeto de Verão, Sol e Natureza</option>
|
||||
<option value="alfabeto">🔤 Brincando com Rimas, Letras e Sons</option>
|
||||
<option value="emocoes">💖 Roda dos Sentimentos e Acolhimento</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<label style="color: var(--text-secondary); font-size: 0.82rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">4. Tema da Música</label>
|
||||
<input type="text" id="musicaTemaInput" placeholder="Ex: amizade na escola, lavar as mãos, alfabeto..." class="obs-input" style="width: 100%; margin-top: 4px; padding: 10px;">
|
||||
</div>
|
||||
|
||||
<button type="button" id="btnGenerateMusica" style="width: 100%; padding: 12px; background: #a855f7; color: white; border: none; border-radius: 10px; font-weight: 700; font-size: 1rem; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; box-shadow: 0 4px 12px rgba(168, 85, 247, 0.3); transition: all 0.2s;">🎵 Compor Música Pedagógica</button>
|
||||
|
||||
<div id="musicaGenerateLoader" style="display: none; align-items: center; justify-content: center; gap: 10px; margin-top: 15px;">
|
||||
<div class="spinner"></div>
|
||||
<span style="color: var(--text-secondary); font-size: 0.9rem;">Compondo a letra...</span>
|
||||
</div>
|
||||
|
||||
<div id="musicaResultArea" style="display: none; flex-direction: column; gap: 12px; margin-top: 20px;">
|
||||
<div style="background: rgba(16, 185, 129, 0.05); border: 1px solid rgba(16, 185, 129, 0.2); padding: 15px; border-radius: 8px;">
|
||||
<h4 style="color: var(--brand-green); margin-top: 0; margin-bottom: 10px;">Letra Gerada:</h4>
|
||||
<div id="musicaTextOutput" style="font-size: 0.95rem; line-height: 1.6; white-space: pre-wrap; color: var(--text-primary);"></div>
|
||||
<!-- TEMA / DESCRIÇÃO -->
|
||||
<div class="settings-group">
|
||||
<label style="color: var(--text-secondary); font-size: 0.75rem; font-weight: 600; text-transform: uppercase;">Tema ou Ideia Central</label>
|
||||
<textarea id="musicaTemaInput" placeholder="Ex: Plantar sementinhas na horta da escola, regar com carinho e colher juntos..." class="obs-input" style="width: 100%; min-height: 60px; padding: 8px; margin-top: 4px; font-size: 0.85rem; resize: vertical;"></textarea>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; justify-content: flex-end;">
|
||||
<button id="btnCopyMusica" style="background: var(--bg-secondary); border: 1px solid var(--border-light); color: var(--text-primary); padding: 8px 16px; border-radius: 6px; cursor: pointer;">📋 Copiar</button>
|
||||
<button id="btnDownloadMusicaPdf" style="background: var(--brand-green); border: none; color: white; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-weight: bold;">📄 Salvar PDF</button>
|
||||
|
||||
<!-- TURMA & FAIXA ETÁRIA -->
|
||||
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||
<div class="settings-group" style="flex: 1; min-width: 140px;">
|
||||
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Vincular à Turma</label>
|
||||
<select id="musicaTurmaSelect" class="obs-select" style="width: 100%; margin-top: 4px;">
|
||||
<option value="">-- Selecionar Turma --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="settings-group" style="flex: 1; min-width: 140px;">
|
||||
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Público / Idade</label>
|
||||
<select id="musicaFaixaEtaria" class="obs-select" style="width: 100%; margin-top: 4px;">
|
||||
<option value="Bebês (0 a 1 ano e 6 meses)">Bebês (0 a 1a6m)</option>
|
||||
<option value="Crianças bem pequenas (1 ano e 7 meses a 3 anos e 11 meses)">Bem pequenas (1a7m a 3a11m)</option>
|
||||
<option value="Crianças pequenas (4 anos a 5 anos e 11 meses)" selected>Crianças pequenas (4 a 5a11m)</option>
|
||||
<option value="Ensino Fundamental I (6 a 10 anos)">Ensino Fundamental I</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RITMO & INTENSIDADE -->
|
||||
<h4 style="margin: 0; color: var(--text-primary); border-bottom: 1px dashed var(--border-light); padding-bottom: 6px; margin-top: 4px; font-size: 0.92rem;">⚙️ 2. Parâmetros Técnicos & Estilo</h4>
|
||||
|
||||
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||
<div class="settings-group" style="flex: 1; min-width: 140px;">
|
||||
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Ritmo / Gênero</label>
|
||||
<select id="musicaRitmo" class="obs-select" style="width: 100%; margin-top: 4px;">
|
||||
<option value="Cantiga de Roda" selected>⭕ Cantiga de Roda</option>
|
||||
<option value="Pop Infantil Alegre">⚡ Pop Infantil Alegre</option>
|
||||
<option value="Canção de Ninar">🌙 Canção de Ninar / Calma</option>
|
||||
<option value="Forrózinho Lúdico">🪗 Forrózinho Lúdico</option>
|
||||
<option value="Samba Infantil">🥁 Samba Infantil</option>
|
||||
<option value="Rockzinho Infantil">🎸 Rockzinho Alegre</option>
|
||||
<option value="Marchinha Escolar">🎺 Marchinha Escolar</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="settings-group" style="flex: 1; min-width: 140px;">
|
||||
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Intensidade</label>
|
||||
<select id="musicaIntensidade" class="obs-select" style="width: 100%; margin-top: 4px;">
|
||||
<option value="calma">🍃 Calma / Suave</option>
|
||||
<option value="moderada" selected>🌟 Moderada / Aconchegante</option>
|
||||
<option value="dancante">🎉 Dançante / Energética</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- INSTRUMENTOS (CHECKBOXES VISUAIS) -->
|
||||
<div class="settings-group">
|
||||
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Instrumentos Empregados</label>
|
||||
<div id="musicaInstrumentsContainer" style="display: flex; gap: 6px; flex-wrap: wrap; margin-top: 4px;">
|
||||
<label class="tag-checkbox"><input type="checkbox" value="Violão acústico" checked> 🎸 Violão</label>
|
||||
<label class="tag-checkbox"><input type="checkbox" value="Pandeiro e percussão" checked> 🪘 Pandeiro</label>
|
||||
<label class="tag-checkbox"><input type="checkbox" value="Flauta doce" checked> 🪈 Flauta</label>
|
||||
<label class="tag-checkbox"><input type="checkbox" value="Xilofone infantil"> 🪵 Xilofone</label>
|
||||
<label class="tag-checkbox"><input type="checkbox" value="Metalofone e sinos"> 🔔 Sinos</label>
|
||||
<label class="tag-checkbox"><input type="checkbox" value="Acordeon"> 🪗 Acordeon</label>
|
||||
<label class="tag-checkbox"><input type="checkbox" value="Teclado e sintetizador"> 🎹 Teclado</label>
|
||||
<label class="tag-checkbox"><input type="checkbox" value="Palmas e coro de crianças"> 👏 Palmas</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- VOZ & DURAÇÃO -->
|
||||
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||
<div class="settings-group" style="flex: 1; min-width: 140px;">
|
||||
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Voz Cantada</label>
|
||||
<select id="musicaVozSelect" class="obs-select" style="width: 100%; margin-top: 4px;">
|
||||
<option value="mulher" selected>👩 Voz Feminina (Acolhedora)</option>
|
||||
<option value="homem">👨 Voz Masculina (Gentil)</option>
|
||||
<option value="crianca">🧒 Voz de Criança (Lúdica)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="settings-group" style="flex: 1; min-width: 140px;">
|
||||
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Duração Recomendada</label>
|
||||
<select id="musicaDuracaoSelect" class="obs-select" style="width: 100%; margin-top: 4px;">
|
||||
<option value="30s">⏱️ 30s (Rascunho Rápido)</option>
|
||||
<option value="1min" selected>⏱️ 1 a 2 min (Ideal 0-5 anos)</option>
|
||||
<option value="2min">⏱️ 2 a 3 min (Completa)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BOTÃO GERAR MÚSICA -->
|
||||
<button type="button" id="btnGenerateMusica" style="width: 100%; padding: 12px; background: linear-gradient(135deg, #a855f7 0%, #7c3aed 100%); color: white; border: none; border-radius: 10px; font-weight: 700; font-size: 0.95rem; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; box-shadow: 0 4px 14px rgba(124, 58, 237, 0.3); transition: all 0.2s; margin-top: 6px;">
|
||||
<span>🎵 Compor Músicas (3 Versões + Áudio)</span>
|
||||
</button>
|
||||
|
||||
<div id="musicaGenerateLoader" style="display: none; align-items: center; justify-content: center; gap: 10px; color: var(--text-secondary); font-size: 0.85rem; padding: 10px; background: rgba(255, 255, 255, 0.02); border-radius: 8px;">
|
||||
<div class="media-spinner" style="width: 18px; height: 18px; border-width: 2px; border-top-color: #a855f7;"></div>
|
||||
<span id="musicaLoaderText">Compondo 3 versões e sintetizando áudio...</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Lado Direito: Apresentação em Cards das 3 Versões -->
|
||||
<div id="musicaRightPanel" style="flex: 1.3; min-width: 360px; display: flex; flex-direction: column; gap: 14px;">
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-light); padding-bottom: 6px;">
|
||||
<h4 style="margin: 0; color: var(--text-primary); font-size: 0.95rem;">🎼 Resultados & Versões Pedagógicas</h4>
|
||||
<div id="musicaBnccBadge" style="display: none; font-size: 0.72rem; background: rgba(59, 130, 246, 0.15); color: #60a5fa; border: 1px solid rgba(59, 130, 246, 0.3); padding: 3px 8px; border-radius: 6px; font-weight: 600;">
|
||||
BNCC: EI02TS02
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ÁREA DE REPRODUÇÃO DE ÁUDIO COM EQUALIZER / WAVEFORM -->
|
||||
<div id="musicaAudioPlayerBox" style="display: none; background: linear-gradient(135deg, rgba(168,85,247,0.12), rgba(59,130,246,0.08)); border: 1px solid rgba(168,85,247,0.3); border-radius: 12px; padding: 14px; flex-direction: column; gap: 10px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span style="font-size: 1.2rem;">🎧</span>
|
||||
<div>
|
||||
<div style="font-size: 0.85rem; font-weight: 700; color: var(--text-primary);" id="musicaPlayerTitle">Música Gerada por PedagogIA</div>
|
||||
<div style="font-size: 0.72rem; color: var(--text-secondary);" id="musicaPlayerSubtitle">Áudio Cantado • Formato MP3</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 6px; align-items: center;">
|
||||
<button type="button" id="btnToggleMusicLoop" class="btn-music-option" style="padding: 4px 8px; font-size: 0.72rem; margin: 0;" title="Repetir em loop na sala de aula">
|
||||
🔁 Loop: Desligado
|
||||
</button>
|
||||
<a id="btnDownloadMusicaAudio" href="#" download="musica_pedagogica.mp3" style="background: #10b981; color: white; border: none; padding: 5px 10px; border-radius: 6px; font-size: 0.75rem; font-weight: 700; text-decoration: none; display: flex; align-items: center; gap: 4px;">
|
||||
📥 Baixar MP3
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Waveform / Equalizer Animado -->
|
||||
<div id="musicaWaveformVisualizer" style="display: flex; align-items: center; justify-content: center; gap: 3px; height: 32px; padding: 4px; background: rgba(0,0,0,0.15); border-radius: 6px;">
|
||||
<div class="wave-bar" style="width: 4px; height: 30%; background: #a855f7; border-radius: 2px;"></div>
|
||||
<div class="wave-bar" style="width: 4px; height: 60%; background: #a855f7; border-radius: 2px;"></div>
|
||||
<div class="wave-bar" style="width: 4px; height: 90%; background: #a855f7; border-radius: 2px;"></div>
|
||||
<div class="wave-bar" style="width: 4px; height: 45%; background: #a855f7; border-radius: 2px;"></div>
|
||||
<div class="wave-bar" style="width: 4px; height: 75%; background: #a855f7; border-radius: 2px;"></div>
|
||||
<div class="wave-bar" style="width: 4px; height: 35%; background: #a855f7; border-radius: 2px;"></div>
|
||||
<div class="wave-bar" style="width: 4px; height: 85%; background: #a855f7; border-radius: 2px;"></div>
|
||||
<div class="wave-bar" style="width: 4px; height: 50%; background: #a855f7; border-radius: 2px;"></div>
|
||||
<div class="wave-bar" style="width: 4px; height: 95%; background: #a855f7; border-radius: 2px;"></div>
|
||||
<div class="wave-bar" style="width: 4px; height: 40%; background: #a855f7; border-radius: 2px;"></div>
|
||||
</div>
|
||||
|
||||
<audio id="musicaAudioPlayer" controls style="width: 100%; margin-top: 2px;"></audio>
|
||||
</div>
|
||||
|
||||
<!-- ABAS DAS 3 VERSÕES -->
|
||||
<div id="musicaVersionsTabs" style="display: none; gap: 6px; border-bottom: 1px solid var(--border-light); padding-bottom: 8px;">
|
||||
<button type="button" id="btnTabVersaoPrincipal" class="tab-btn active" style="font-size: 0.78rem; padding: 6px 12px; background: rgba(168,85,247,0.15); border: none; cursor: pointer; color: #c084fc; border-radius: 6px; font-weight: bold;">🌟 Versão Principal</button>
|
||||
<button type="button" id="btnTabVersaoCurta" class="tab-btn" style="font-size: 0.78rem; padding: 6px 12px; background: transparent; border: none; cursor: pointer; color: var(--text-secondary); border-radius: 6px;">⚡ Rascunho / Curta</button>
|
||||
<button type="button" id="btnTabVersaoDancante" class="tab-btn" style="font-size: 0.78rem; padding: 6px 12px; background: transparent; border: none; cursor: pointer; color: var(--text-secondary); border-radius: 6px;">🎉 Dançante / Roda</button>
|
||||
</div>
|
||||
|
||||
<!-- ÁREA DO CONTEÚDO DA VERSÃO ATIVA -->
|
||||
<div id="musicaVersionDisplayArea" style="display: flex; flex-direction: column; gap: 12px; flex: 1;">
|
||||
|
||||
<div id="musicaPlaceholderText" style="text-align: center; padding: 40px 20px; color: var(--text-secondary); background: rgba(255,255,255,0.02); border: 1px dashed var(--border-light); border-radius: 10px;">
|
||||
<span style="font-size: 2.5rem; display: block; margin-bottom: 10px;">🎼</span>
|
||||
<strong style="color: var(--text-primary); font-size: 0.95rem;">Pronto para transformar sua ideia em música?</strong>
|
||||
<p style="font-size: 0.82rem; margin-top: 6px; opacity: 0.8;">Escolha um modelo ou digite o tema ao lado e clique em "Compor Músicas" para gerar as 3 versões com áudio e referências BNCC.</p>
|
||||
</div>
|
||||
|
||||
<!-- CONTAINER DA LETRA ATIVA -->
|
||||
<div id="musicaActiveContentBox" style="display: none; background: rgba(255,255,255,0.03); border: 1px solid var(--border-light); border-radius: 12px; padding: 16px; flex-direction: column; gap: 12px;">
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px dashed var(--border-light); padding-bottom: 8px;">
|
||||
<div>
|
||||
<h4 id="musicaActiveTitle" style="margin: 0; color: #a855f7; font-size: 1.05rem; font-family: 'Fredoka', sans-serif;">Título da Música</h4>
|
||||
<span id="musicaActiveBadge" style="font-size: 0.72rem; color: var(--text-secondary);">Versão Principal</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 6px;">
|
||||
<button type="button" id="btnCopyActiveLyrics" style="background: transparent; border: 1px solid var(--border-light); color: var(--text-secondary); padding: 4px 8px; border-radius: 6px; font-size: 0.72rem; cursor: pointer;" title="Copiar letra">📋 Copiar</button>
|
||||
<button type="button" id="btnDownloadActiveTxt" style="background: transparent; border: 1px solid var(--border-light); color: var(--text-secondary); padding: 4px 8px; border-radius: 6px; font-size: 0.72rem; cursor: pointer;" title="Baixar TXT">💾 TXT</button>
|
||||
<button type="button" id="btnExportActivePdf" style="background: #3b82f6; color: white; border: none; padding: 4px 10px; border-radius: 6px; font-size: 0.72rem; font-weight: 700; cursor: pointer;" title="Salvar PDF formatado">📄 PDF</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Letra Formatada -->
|
||||
<div id="musicaActiveLyrics" style="font-family: 'Fredoka', sans-serif; font-size: 1.05rem; line-height: 1.7; color: var(--text-primary); white-space: pre-wrap; background: rgba(0,0,0,0.08); padding: 14px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.05); max-height: 280px; overflow-y: auto;"></div>
|
||||
|
||||
<!-- Dica Pedagógica -->
|
||||
<div id="musicaPedagogicalTipBox" style="background: rgba(16, 185, 129, 0.08); border: 1px solid rgba(16, 185, 129, 0.2); border-radius: 8px; padding: 10px 14px; font-size: 0.8rem; color: #a7f3d0; line-height: 1.4;">
|
||||
💡 <strong>Dica Pedagógica:</strong> <span id="musicaPedagogicalTipText">Estimule a coordenação motora das crianças durante o refrão.</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Histórico de Músicas -->
|
||||
<div id="musicaHistoryModal" class="settings-modal" style="display: none; z-index: 10001;">
|
||||
<div class="settings-modal-content" style="max-width: 500px; width: 95%; max-height: 80vh; display: flex; flex-direction: column;">
|
||||
<div class="settings-modal-header" style="background: var(--bg-secondary); border-bottom: 1px solid var(--border-light);">
|
||||
<h3 style="display: flex; align-items: center; gap: 8px; font-family: 'Outfit', sans-serif;">🕒 Histórico de Músicas</h3>
|
||||
<button id="btnCloseMusicaHistory" class="btn-close-modal" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.5rem;">×</button>
|
||||
<div class="settings-modal-content" style="max-width: 540px; width: 95%; max-height: 80vh; display: flex; flex-direction: column;">
|
||||
<div class="settings-modal-header" style="background: var(--bg-secondary); border-bottom: 1px solid var(--border-light); padding: 14px 20px; display: flex; justify-content: space-between; align-items: center;">
|
||||
<h3 style="margin: 0; display: flex; align-items: center; gap: 8px; font-family: 'Outfit', sans-serif; font-size: 1.1rem;">🕒 Histórico de Músicas Geradas</h3>
|
||||
<button type="button" id="btnCloseMusicaHistory" class="btn-close-modal" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.5rem; line-height: 1;">×</button>
|
||||
</div>
|
||||
<div id="musicaHistoryList" style="padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 12px;"></div>
|
||||
<div id="musicaHistoryList" style="padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 12px; flex: 1;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4067,6 +4067,79 @@ code {
|
||||
border-color: rgba(139, 92, 246, 0.4) !important;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
ESTILOS DO MUSICANDO IDEIAS (ESTÚDIO PROFISSIONAL)
|
||||
============================================================ */
|
||||
.tag-checkbox {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 6px;
|
||||
padding: 5px 9px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.tag-checkbox:hover {
|
||||
background: rgba(168, 85, 247, 0.12);
|
||||
border-color: rgba(168, 85, 247, 0.4);
|
||||
}
|
||||
|
||||
.tag-checkbox:has(input:checked) {
|
||||
background: rgba(168, 85, 247, 0.2);
|
||||
border-color: #a855f7;
|
||||
color: #d8b4fe;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tag-checkbox input[type="checkbox"] {
|
||||
accent-color: #a855f7;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Animação do Visualizer de Áudio */
|
||||
.wave-bar {
|
||||
transition: height 0.15s ease;
|
||||
}
|
||||
|
||||
.wave-bar.wave-active {
|
||||
animation: wave-pulse 0.8s infinite ease-in-out alternate;
|
||||
}
|
||||
|
||||
.wave-bar.wave-active:nth-child(2n) {
|
||||
animation-duration: 0.6s;
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
|
||||
.wave-bar.wave-active:nth-child(3n) {
|
||||
animation-duration: 0.9s;
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.wave-bar.wave-active:nth-child(4n) {
|
||||
animation-duration: 0.7s;
|
||||
animation-delay: 0.15s;
|
||||
}
|
||||
|
||||
@keyframes wave-pulse {
|
||||
0% { height: 20%; background-color: #a855f7; }
|
||||
50% { height: 95%; background-color: #ec4899; }
|
||||
100% { height: 40%; background-color: #3b82f6; }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#musicaLeftPanel, #musicaRightPanel {
|
||||
min-width: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user