feat: Estudio Musical para criacao de cantigas personalizadas
This commit is contained in:
+186
@@ -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
|
||||
|
||||
+95
-4
@@ -99,6 +99,14 @@
|
||||
</svg>
|
||||
<span>Modelos de Relatório</span>
|
||||
</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>
|
||||
|
||||
<!-- Divisor -->
|
||||
@@ -210,10 +218,10 @@
|
||||
<div class="card-title">Planejamento Mind Lab</div>
|
||||
<div class="card-desc">Atividades lúdicas para desenvolvimento de habilidades socioemocionais.</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="card-icon">🎵</div>
|
||||
<div class="card-title">Criar Música / Cantiga</div>
|
||||
<div class="card-desc">Gere músicas divertidas com melodia e letra educativa.</div>
|
||||
<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-title" style="color: #c084fc;">Estúdio Musical</div>
|
||||
<div class="card-desc">Gere músicas cantadas personalizadas com voz, ritmo e duração.</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="card-icon">🎨</div>
|
||||
@@ -637,10 +645,93 @@
|
||||
Escolha uma criança e um modelo de relatório para começar.
|
||||
</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;">×</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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user