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.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Compondo a letra e gravando os vocais...
+
+
+
+
+
+
✨ Sua Canção Está Pronta!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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();