diff --git a/public/app.js b/public/app.js index 6c86ecc..9c37ac7 100644 --- a/public/app.js +++ b/public/app.js @@ -3256,6 +3256,192 @@ const initApp = () => { printWindow.document.close(); }); } + + // ============================================================ + // ESTÚDIO MUSICAL (Criação de música cantada com MiniMax) + // ============================================================ + const estudioMusicalModal = document.getElementById('estudioMusicalModal'); + const btnCloseEstudioModal = document.getElementById('btnCloseEstudioModal'); + const sidebarBtnEstudio = document.getElementById('sidebarBtnEstudio'); + const welcomeCardEstudio = document.getElementById('welcomeCardEstudio'); + + const btnGenerateStudioMusic = document.getElementById('btnGenerateStudioMusic'); + const musicTemaInput = document.getElementById('musicTemaInput'); + const musicGenerateLoader = document.getElementById('musicGenerateLoader'); + const musicLoaderText = document.getElementById('musicLoaderText'); + const musicResultArea = document.getElementById('musicResultArea'); + const musicStudioAudioPlayer = document.getElementById('musicStudioAudioPlayer'); + const musicStudioDownloadBtn = document.getElementById('musicStudioDownloadBtn'); + const musicStudioLyricsArea = document.getElementById('musicStudioLyricsArea'); + const btnCopyMusicLyrics = document.getElementById('btnCopyMusicLyrics'); + const btnDownloadMusicLyrics = document.getElementById('btnDownloadMusicLyrics'); + + let selectedVoice = 'mulher'; + let selectedRhythm = 'roda'; + let selectedDuration = '30s'; + + // Toggle classes active para botões de opções + document.querySelectorAll('.btn-music-option').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('.btn-music-option').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + selectedVoice = btn.dataset.voice; + }); + }); + + document.querySelectorAll('.btn-music-rhythm').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('.btn-music-rhythm').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + selectedRhythm = btn.dataset.rhythm; + }); + }); + + document.querySelectorAll('.btn-music-duration').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('.btn-music-duration').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + selectedDuration = btn.dataset.duration; + }); + }); + + // Abrir e fechar modal + const openEstudioModal = () => { + estudioMusicalModal.style.display = 'flex'; + musicTemaInput.value = ''; + musicResultArea.style.display = 'none'; + musicStudioAudioPlayer.src = ''; + musicStudioLyricsArea.textContent = ''; + if (musicGenerateLoader) musicGenerateLoader.style.display = 'none'; + btnGenerateStudioMusic.disabled = false; + }; + + if (sidebarBtnEstudio) { + sidebarBtnEstudio.addEventListener('click', openEstudioModal); + } + + if (welcomeCardEstudio) { + welcomeCardEstudio.addEventListener('click', openEstudioModal); + } + + if (btnCloseEstudioModal) { + btnCloseEstudioModal.addEventListener('click', () => { + estudioMusicalModal.style.display = 'none'; + musicStudioAudioPlayer.pause(); + }); + } + + // Fechar modal clicando fora + window.addEventListener('click', (e) => { + if (e.target === estudioMusicalModal) { + estudioMusicalModal.style.display = 'none'; + musicStudioAudioPlayer.pause(); + } + }); + + // Ação de gerar música no estúdio + if (btnGenerateStudioMusic) { + btnGenerateStudioMusic.addEventListener('click', async () => { + const tema = musicTemaInput.value.trim(); + if (!tema) { + alert('Por favor, digite um tema para a música.'); + return; + } + + btnGenerateStudioMusic.disabled = true; + musicGenerateLoader.style.display = 'flex'; + musicResultArea.style.display = 'none'; + + const statusInterval = setInterval(() => { + const statusMessages = [ + 'Compondo a letra pedagógica...', + 'Ajustando a métrica e as rimas...', + 'Gerando o arranjo musical...', + 'Adicionando os vocais cantados...', + 'Quase pronto, finalizando o áudio...' + ]; + const randomMsg = statusMessages[Math.floor(Math.random() * statusMessages.length)]; + musicLoaderText.textContent = randomMsg; + }, 5000); + + try { + const response = await fetch('/api/music/generate', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + tema, + voz: selectedVoice, + ritmo: selectedRhythm, + duracao: selectedDuration + }) + }); + + clearInterval(statusInterval); + + if (!response.ok) { + const errData = await response.json(); + throw new Error(errData.error || 'Erro na geração de áudio'); + } + + const data = await response.json(); + + // Exibir resultados + musicStudioLyricsArea.textContent = data.lyrics; + musicStudioAudioPlayer.src = data.audioUrl; + musicStudioDownloadBtn.href = data.audioUrl; + + musicGenerateLoader.style.display = 'none'; + musicResultArea.style.display = 'flex'; + + } catch (err) { + clearInterval(statusInterval); + console.error('Erro ao gerar música no estúdio:', err); + alert('Erro ao gerar música: ' + err.message); + musicGenerateLoader.style.display = 'none'; + btnGenerateStudioMusic.disabled = false; + } + }); + } + + // Copiar letra + if (btnCopyMusicLyrics) { + btnCopyMusicLyrics.addEventListener('click', () => { + const lyrics = musicStudioLyricsArea.textContent; + if (lyrics) { + navigator.clipboard.writeText(lyrics) + .then(() => { + btnCopyMusicLyrics.textContent = '✅ Copiado!'; + setTimeout(() => { + btnCopyMusicLyrics.textContent = '📋 Copiar Letra'; + }, 2000); + }) + .catch(err => { + console.error('Falha ao copiar:', err); + }); + } + }); + } + + // Baixar letra em arquivo de texto (TXT) + if (btnDownloadMusicLyrics) { + btnDownloadMusicLyrics.addEventListener('click', () => { + const lyrics = musicStudioLyricsArea.textContent; + const tema = musicTemaInput.value.trim() || 'cantiga'; + if (lyrics) { + const blob = new Blob([lyrics], { type: 'text/plain;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `letra_${tema.replace(/\s+/g, '_').toLowerCase()}.txt`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + }); + } }; // Player de áudio personalizado global para as mídias geradas diff --git a/public/index.html b/public/index.html index f009042..9d9f0f2 100644 --- a/public/index.html +++ b/public/index.html @@ -99,6 +99,14 @@ Modelos de Relatório + + + @@ -210,10 +218,10 @@
Planejamento Mind Lab
Atividades lúdicas para desenvolvimento de habilidades socioemocionais.
-
-
🎵
-
Criar Música / Cantiga
-
Gere músicas divertidas com melodia e letra educativa.
+
+
🎹
+
Estúdio Musical
+
Gere músicas cantadas personalizadas com voz, ritmo e duração.
🎨
@@ -637,10 +645,93 @@ Escolha uma criança e um modelo de relatório para começar.
+ + + diff --git a/public/style.css b/public/style.css index 659e026..33b76d6 100644 --- a/public/style.css +++ b/public/style.css @@ -3083,3 +3083,47 @@ code { color: var(--brand-green, #10a37f); font-weight: 600; } + +/* ========================================================================== + ESTÚDIO MUSICAL + ========================================================================== */ +.btn-music-option, .btn-music-rhythm, .btn-music-duration { + background-color: var(--bg-secondary); + border: 1px solid var(--border-light); + color: var(--text-secondary); + border-radius: 8px; + padding: 10px; + font-size: 0.85rem; + font-weight: 500; + cursor: pointer; + transition: all var(--transition-fast) ease; + text-align: center; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; +} + +.btn-music-option:hover, .btn-music-rhythm:hover, .btn-music-duration:hover { + background-color: var(--bg-tertiary); + color: var(--text-primary); + border-color: rgba(168, 85, 247, 0.5); + transform: translateY(-1px); +} + +.btn-music-option.active, .btn-music-rhythm.active, .btn-music-duration.active { + background-color: rgba(168, 85, 247, 0.15); + border-color: #a855f7; + color: #c084fc; + font-weight: 600; + box-shadow: 0 0 8px rgba(168, 85, 247, 0.2); +} + +/* Customização para botões de tags ativos no estúdio */ +.btn-music-duration.active { + background-color: rgba(16, 163, 127, 0.15); + border-color: var(--brand-green); + color: var(--brand-green); + box-shadow: 0 0 8px rgba(16, 163, 127, 0.2); +} + diff --git a/scratch/test_music_generation.js b/scratch/test_music_generation.js new file mode 100644 index 0000000..6aa6b39 --- /dev/null +++ b/scratch/test_music_generation.js @@ -0,0 +1,49 @@ +require('dotenv').config(); + +async function testMusic() { + const apiKey = process.env.MINIMAX_API_KEY; + const apiBase = (process.env.MINIMAX_API_BASE || 'https://api.minimax.io/v1').replace(/\/v1\/?$/, ''); + + console.log('API Base:', apiBase); + console.log('API Key (início):', apiKey ? apiKey.substring(0, 15) + '...' : 'não definida'); + + const payload = { + model: 'music-2.0', + prompt: 'happy acoustic children song, simple melody, cute', + lyrics: 'Vamos brincar e cantar, na escola vamos estudar!', + audio_setting: { sample_rate: 32000, bitrate: 128000, format: 'mp3' } + }; + + try { + console.log('Enviando requisição de geração de música...'); + const response = await fetch(`${apiBase}/v1/music_generation`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }); + + console.log('Status da resposta:', response.status); + + if (!response.ok) { + const errText = await response.text(); + console.error('Erro retornado pela API da MiniMax:', errText); + return; + } + + const data = await response.json(); + console.log('Sucesso! Resposta da API:', JSON.stringify(data, null, 2).substring(0, 1000)); + + if (data.data && data.data.audio) { + console.log('Áudio retornado em formato hexadecimal. Tamanho:', data.data.audio.length); + } else { + console.log('Nenhum dado de áudio encontrado no JSON de resposta.'); + } + } catch (error) { + console.error('Erro ao conectar ou processar requisição:', error); + } +} + +testMusic(); diff --git a/server.js b/server.js index f663d0d..133aae8 100644 --- a/server.js +++ b/server.js @@ -210,6 +210,113 @@ app.post('/api/tts', requireAuth, async (req, res) => { } }); +// Rota POST para gerar música customizada (Estúdio Musical) +app.post('/api/music/generate', requireAuth, async (req, res) => { + const { tema, voz, ritmo, duracao } = req.body; + if (!tema) { + return res.status(400).json({ error: 'Tema da música é obrigatório' }); + } + + try { + // 1. Determinar o prompt de estilo baseado nos inputs + let voiceDesc = 'female vocals, clear and sweet singing voice'; + if (voz === 'homem') { + voiceDesc = 'male vocals, warm and gentle singing voice'; + } else if (voz === 'crianca') { + voiceDesc = 'children vocals, sweet child singing voice'; + } + + let rhythmDesc = 'happy acoustic children song, simple melody, acoustic guitar'; + if (ritmo === 'roda') { + rhythmDesc = 'traditional Brazilian cantiga de roda, folk children rhythm, acoustic guitar, percussion'; + } else if (ritmo === 'pop') { + rhythmDesc = 'upbeat pop for children, modern synth, happy rhythm, claps'; + } else if (ritmo === 'ninar') { + rhythmDesc = 'gentle lullaby for children, music box, soft strings, calm, sleeping mood'; + } else if (ritmo === 'marcha') { + rhythmDesc = 'marching children rhythm, playful snare drums, acoustic guitar, brass accent'; + } + + const stylePrompt = `${rhythmDesc}, ${voiceDesc}, educational children song style, clear pronunciation, 100 BPM`; + + // 2. Determinar o tamanho das letras baseado na duração (duracao) + let lengthInstructions = 'curta, de apenas 6 a 8 versos'; + if (duracao === '1min') { + lengthInstructions = 'média, de 12 a 16 versos (com verso 1, refrão, verso 2, refrão)'; + } else if (duracao === '2min') { + lengthInstructions = 'completa e longa, de 24 a 32 versos (com estrutura completa: verso 1, refrão, verso 2, refrão, ponte e refrão final)'; + } + + // 3. Chamar a IA (MiniMax-M3) para compor a letra + const lyricsPrompt = `Gere uma letra de cantiga escolar infantil sobre o tema: "${tema}". +A letra deve ser escrita para o público infantil (linguagem lúdica, alegre e educativa). +A estrutura de versos deve ser: ${lengthInstructions}. +Retorne APENAS a letra formatada em versos com quebras de linha. Não adicione notas, comentários ou títulos.`; + + const aiResponse = await callMinimax({ + system: "Você é um compositor pedagógico infantil que escreve letras de músicas educativas em português brasileiro. Retorne estritamente a letra da música, sem introduções ou observações extras.", + messages: [{ role: 'user', content: lyricsPrompt }], + temperature: 0.7, + max_tokens: 1000 + }); + + const lyrics = aiResponse.text ? aiResponse.text.trim() : ''; + if (!lyrics) { + throw new Error('Falha ao compor a letra da música.'); + } + + // 4. Chamar a API da MiniMax para gerar a música (canto + melodia) + 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: stylePrompt, + lyrics: lyrics, + audio_setting: { sample_rate: 32000, bitrate: 128000, format: 'mp3' } + }) + }); + + if (!musicResp.ok) { + const errText = await musicResp.text(); + throw new Error(`MiniMax music-2.0 respondeu erro: ${errText}`); + } + + const musicData = await musicResp.json(); + const audioHex = musicData.data?.audio; + if (!audioHex) { + throw new Error('Nenhum dado de áudio retornado pela MiniMax.'); + } + + // 5. Salvar o arquivo localmente + 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 = `estudio_musica_${Date.now()}.mp3`; + const filePath = path.join(mediaDir, fileName); + fs.writeFileSync(filePath, Buffer.from(audioHex, 'hex')); + const audioUrl = `/generated-media/${fileName}`; + + // 6. Retornar dados para o frontend + res.json({ + success: true, + lyrics: lyrics, + audioUrl: audioUrl + }); + + } catch (error) { + console.error('Erro no Estúdio Musical:', error); + res.status(500).json({ error: error.message || 'Ocorreu um erro ao gerar a música' }); + } +}); + // Rota GET para obter configurações do agente app.get('/api/agent-config', requireAuth, (req, res) => { const config = readAgentConfig();