feat: Estudio Musical para criacao de cantigas personalizadas

This commit is contained in:
2026-06-03 11:21:23 +00:00
parent 6a1c89bb33
commit 22f34b2e35
5 changed files with 481 additions and 4 deletions
+186
View File
@@ -3256,6 +3256,192 @@ const initApp = () => {
printWindow.document.close(); 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 // Player de áudio personalizado global para as mídias geradas
+95 -4
View File
@@ -99,6 +99,14 @@
</svg> </svg>
<span>Modelos de Relatório</span> <span>Modelos de Relatório</span>
</button> </button>
<!-- Botão: Estúdio Musical -->
<button class="btn-observacoes" id="sidebarBtnEstudio" style="border-color: #a855f7; color: #a855f7; background: rgba(168, 85, 247, 0.05);">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 19V6l12-3v13M9 10l12-3M9 14l12-3" />
</svg>
<span>Estúdio Musical</span>
</button>
</div> </div>
<!-- Divisor --> <!-- Divisor -->
@@ -210,10 +218,10 @@
<div class="card-title">Planejamento Mind Lab</div> <div class="card-title">Planejamento Mind Lab</div>
<div class="card-desc">Atividades lúdicas para desenvolvimento de habilidades socioemocionais.</div> <div class="card-desc">Atividades lúdicas para desenvolvimento de habilidades socioemocionais.</div>
</div> </div>
<div class="suggestion-card" data-prompt="Crie uma música pedagógica infantil alegre e animada sobre animais e sons da fazenda."> <div class="suggestion-card" id="welcomeCardEstudio" style="border-color: rgba(168, 85, 247, 0.25); background: rgba(168, 85, 247, 0.02);">
<div class="card-icon">🎵</div> <div class="card-icon">🎹</div>
<div class="card-title">Criar Música / Cantiga</div> <div class="card-title" style="color: #c084fc;">Estúdio Musical</div>
<div class="card-desc">Gere músicas divertidas com melodia e letra educativa.</div> <div class="card-desc">Gere músicas cantadas personalizadas com voz, ritmo e duração.</div>
</div> </div>
<div class="suggestion-card" data-prompt="Crie um planejamento pedagógico de artes alinhado à BNCC para o campo 'Traços, sons, cores e formas'."> <div class="suggestion-card" data-prompt="Crie um planejamento pedagógico de artes alinhado à BNCC para o campo 'Traços, sons, cores e formas'.">
<div class="card-icon">🎨</div> <div class="card-icon">🎨</div>
@@ -637,10 +645,93 @@
Escolha uma criança e um modelo de relatório para começar. Escolha uma criança e um modelo de relatório para começar.
</div> </div>
</div> </div>
<!-- Modal: Estúdio Musical (Criação de música cantada com MiniMax) -->
<div id="estudioMusicalModal" 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;">🎵 Estúdio Musical Camila AI</h3>
<button id="btnCloseEstudioModal" class="btn-close-modal" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.5rem;">&times;</button>
</div>
<div class="settings-modal-body" style="padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 18px; flex: 1;">
<!-- VOZ -->
<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. Escolha a Voz do Canto</label>
<div class="music-option-grid" style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 4px;">
<button type="button" class="btn-music-option active" data-voice="mulher">👩 Mulher</button>
<button type="button" class="btn-music-option" data-voice="homem">👨 Homem</button>
<button type="button" class="btn-music-option" data-voice="crianca">🧒 Criança</button>
</div>
</div> </div>
<!-- RITMO -->
<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. Escolha o Ritmo da Cantiga</label>
<div class="music-option-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 10px; margin-top: 4px;">
<button type="button" class="btn-music-rhythm active" data-rhythm="roda">⭕ Cantiga de Roda</button>
<button type="button" class="btn-music-rhythm" data-rhythm="pop">⚡ Pop Infantil</button>
<button type="button" class="btn-music-rhythm" data-rhythm="ninar">🌙 Canção de Ninar</button>
<button type="button" class="btn-music-rhythm" data-rhythm="acustico">🎸 Alegre Acústico</button>
<button type="button" class="btn-music-rhythm" data-rhythm="marcha">🥁 Marcha Rítmica</button>
</div>
</div>
<!-- DURAÇÃO -->
<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. Escolha a Duração da Música</label>
<div class="music-option-grid" style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 4px;">
<button type="button" class="btn-music-duration active" data-duration="30s">⏱️ 30 segundos</button>
<button type="button" class="btn-music-duration" data-duration="1min">⏱️ 1 minuto</button>
<button type="button" class="btn-music-duration" data-duration="2min">⏱️ 2 minutos</button>
</div>
</div>
<!-- TEMA -->
<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. Digite o Tema da Música</label>
<textarea id="musicTemaInput" placeholder="Ex: Higiene pessoal, lavar as mãos antes de comer, os sons dos animais da fazenda..." class="obs-input" style="width: 100%; min-height: 80px; padding: 10px; resize: vertical; margin-top: 4px; line-height: 1.4;"></textarea>
</div>
<!-- BOTÃO GERAR E STATUS -->
<div style="display: flex; flex-direction: column; gap: 10px; margin-top: 8px;">
<button id="btnGenerateStudioMusic" style="background: linear-gradient(135deg, #a855f7 0%, #7c3aed 100%); color: white; border: none; padding: 12px; border-radius: 8px; font-weight: 600; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; transition: opacity 0.2s; box-shadow: 0 4px 12px rgba(124, 58, 237, 0.25);">
<span>🎵 Gerar Canção Personalizada</span>
</button>
<div id="musicGenerateLoader" style="display: none; align-items: center; justify-content: center; gap: 10px; color: var(--text-secondary); font-size: 0.9rem; padding: 10px; background: rgba(255, 255, 255, 0.02); border-radius: 8px;">
<div class="record-spinner" style="width: 20px; height: 20px; border-width: 2px;"></div>
<span id="musicLoaderText">Compondo a letra e gravando os vocais...</span>
</div>
</div>
<!-- ÁREA DE RESULTADO (INICIALMENTE OCULTA) -->
<div id="musicResultArea" style="display: none; border-top: 1px solid var(--border-light); padding-top: 16px; flex-direction: column; gap: 14px;">
<h4 style="margin: 0; color: var(--text-primary); font-size: 1rem; display: flex; align-items: center; gap: 6px;">✨ Sua Canção Está Pronta!</h4>
<!-- Player de Áudio e Download MP3 -->
<div style="background: var(--bg-secondary); padding: 14px; border-radius: 12px; display: flex; flex-direction: column; gap: 12px; border: 1px solid var(--border-light);">
<audio id="musicStudioAudioPlayer" controls style="width: 100%;"></audio>
<a id="musicStudioDownloadBtn" href="#" download="cantiga_pedagogica.mp3" class="btn-save-settings" style="display: flex; align-items: center; justify-content: center; gap: 8px; text-decoration: none; font-size: 0.9rem; padding: 10px; background: linear-gradient(135deg, var(--brand-green) 0%, #0a5f49 100%);">
📥 Baixar Música (MP3)
</a>
</div>
<!-- Letra Transcrita -->
<div style="display: flex; flex-direction: column; gap: 8px;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<label style="color: var(--text-secondary); font-size: 0.78rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">Letra da Música</label>
<div style="display: flex; gap: 8px;">
<button id="btnCopyMusicLyrics" style="background: none; border: 1px solid var(--border-light); color: var(--text-secondary); padding: 5px 10px; border-radius: 6px; font-size: 0.75rem; cursor: pointer; transition: all 0.2s;">📋 Copiar Letra</button>
<button id="btnDownloadMusicLyrics" style="background: none; border: 1px solid var(--border-light); color: var(--text-secondary); padding: 5px 10px; border-radius: 6px; font-size: 0.75rem; cursor: pointer; transition: all 0.2s;">💾 Baixar Letra (TXT)</button>
</div>
</div>
<pre id="musicStudioLyricsArea" style="background: var(--bg-secondary); padding: 14px; border-radius: 10px; max-height: 180px; overflow-y: auto; font-family: inherit; font-size: 0.92rem; line-height: 1.6; white-space: pre-wrap; border: 1px solid var(--border-light); color: var(--text-primary); margin: 0; box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);"></pre>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Bibliotecas externas para renderizar markdown e blocos de código --> <!-- Bibliotecas externas para renderizar markdown e blocos de código -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js"></script>
+44
View File
@@ -3083,3 +3083,47 @@ code {
color: var(--brand-green, #10a37f); color: var(--brand-green, #10a37f);
font-weight: 600; 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);
}
+49
View File
@@ -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();
+107
View File
@@ -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 // Rota GET para obter configurações do agente
app.get('/api/agent-config', requireAuth, (req, res) => { app.get('/api/agent-config', requireAuth, (req, res) => {
const config = readAgentConfig(); const config = readAgentConfig();