feat: Estudio Musical para criacao de cantigas personalizadas
This commit is contained in:
+186
@@ -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
@@ -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,6 +645,89 @@
|
|||||||
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;">×</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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
// 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();
|
||||||
|
|||||||
Reference in New Issue
Block a user