feat: Geração de música cantada com melodia e instrumentos pelo MiniMax music-2.0 e barra de progresso animada em roxo vibrante no player

This commit is contained in:
2026-09-01 10:54:40 +00:00
parent b7e16bb12f
commit 57d5e6e3e1
4 changed files with 111 additions and 32 deletions
+38 -1
View File
@@ -7591,7 +7591,20 @@ window.toggleCustomAudio = (btn) => {
}); });
} }
// Equalizer / Waveform Animation quando toca // Formatação de Tempo (mm:ss)
const formatAudioTime = (sec) => {
if (!sec || isNaN(sec)) return '0:00';
const m = Math.floor(sec / 60);
const s = Math.floor(sec % 60);
return `${m}:${s < 10 ? '0' : ''}${s}`;
};
// Equalizer / Waveform & Barra de Progresso Roxo Vibrante
const musicaCustomProgressContainer = document.getElementById('musicaCustomProgressContainer');
const musicaCustomProgressBar = document.getElementById('musicaCustomProgressBar');
const musicaCurrentTime = document.getElementById('musicaCurrentTime');
const musicaDurationTime = document.getElementById('musicaDurationTime');
if (musicaAudioPlayer) { if (musicaAudioPlayer) {
musicaAudioPlayer.addEventListener('play', () => { musicaAudioPlayer.addEventListener('play', () => {
const bars = document.querySelectorAll('#musicaWaveformVisualizer .wave-bar'); const bars = document.querySelectorAll('#musicaWaveformVisualizer .wave-bar');
@@ -7601,6 +7614,30 @@ window.toggleCustomAudio = (btn) => {
const bars = document.querySelectorAll('#musicaWaveformVisualizer .wave-bar'); const bars = document.querySelectorAll('#musicaWaveformVisualizer .wave-bar');
bars.forEach(b => b.classList.remove('wave-active')); bars.forEach(b => b.classList.remove('wave-active'));
}); });
musicaAudioPlayer.addEventListener('timeupdate', () => {
const cur = musicaAudioPlayer.currentTime || 0;
const dur = musicaAudioPlayer.duration || 0;
if (musicaCurrentTime) musicaCurrentTime.textContent = formatAudioTime(cur);
if (musicaDurationTime && dur) musicaDurationTime.textContent = formatAudioTime(dur);
if (musicaCustomProgressBar && dur > 0) {
const pct = Math.min(100, Math.max(0, (cur / dur) * 100));
musicaCustomProgressBar.style.width = `${pct}%`;
}
});
musicaAudioPlayer.addEventListener('loadedmetadata', () => {
const dur = musicaAudioPlayer.duration || 0;
if (musicaDurationTime) musicaDurationTime.textContent = formatAudioTime(dur);
});
if (musicaCustomProgressContainer) {
musicaCustomProgressContainer.addEventListener('click', (e) => {
const rect = musicaCustomProgressContainer.getBoundingClientRect();
const clickPos = (e.clientX - rect.left) / rect.width;
if (musicaAudioPlayer.duration) {
musicaAudioPlayer.currentTime = clickPos * musicaAudioPlayer.duration;
}
});
}
} }
// --- GERAR MÚSICA (COMPOR 3 VERSÕES + ÁUDIO) --- // --- GERAR MÚSICA (COMPOR 3 VERSÕES + ÁUDIO) ---
+22 -8
View File
@@ -1138,20 +1138,34 @@
</div> </div>
<!-- Waveform / Equalizer Animado --> <!-- Waveform / Equalizer Animado -->
<div id="musicaWaveformVisualizer" style="display: flex; align-items: center; justify-content: center; gap: 3px; height: 32px; padding: 4px; background: rgba(0,0,0,0.15); border-radius: 6px;"> <div id="musicaWaveformVisualizer" style="display: flex; align-items: center; justify-content: center; gap: 4px; height: 36px; padding: 6px 12px; background: rgba(0,0,0,0.3); border-radius: 8px; border: 1px solid rgba(168,85,247,0.25);">
<div class="wave-bar" style="width: 4px; height: 30%; background: #a855f7; border-radius: 2px;"></div> <div class="wave-bar" style="width: 4px; height: 30%; background: #a855f7; border-radius: 2px;"></div>
<div class="wave-bar" style="width: 4px; height: 60%; background: #a855f7; border-radius: 2px;"></div> <div class="wave-bar" style="width: 4px; height: 60%; background: #c084fc; border-radius: 2px;"></div>
<div class="wave-bar" style="width: 4px; height: 90%; background: #a855f7; border-radius: 2px;"></div> <div class="wave-bar" style="width: 4px; height: 90%; background: #e879f9; border-radius: 2px;"></div>
<div class="wave-bar" style="width: 4px; height: 45%; background: #a855f7; border-radius: 2px;"></div> <div class="wave-bar" style="width: 4px; height: 45%; background: #a855f7; border-radius: 2px;"></div>
<div class="wave-bar" style="width: 4px; height: 75%; background: #a855f7; border-radius: 2px;"></div> <div class="wave-bar" style="width: 4px; height: 75%; background: #c084fc; border-radius: 2px;"></div>
<div class="wave-bar" style="width: 4px; height: 35%; background: #a855f7; border-radius: 2px;"></div> <div class="wave-bar" style="width: 4px; height: 35%; background: #e879f9; border-radius: 2px;"></div>
<div class="wave-bar" style="width: 4px; height: 85%; background: #a855f7; border-radius: 2px;"></div> <div class="wave-bar" style="width: 4px; height: 85%; background: #a855f7; border-radius: 2px;"></div>
<div class="wave-bar" style="width: 4px; height: 50%; background: #a855f7; border-radius: 2px;"></div> <div class="wave-bar" style="width: 4px; height: 50%; background: #c084fc; border-radius: 2px;"></div>
<div class="wave-bar" style="width: 4px; height: 95%; background: #a855f7; border-radius: 2px;"></div> <div class="wave-bar" style="width: 4px; height: 95%; background: #e879f9; border-radius: 2px;"></div>
<div class="wave-bar" style="width: 4px; height: 40%; background: #a855f7; border-radius: 2px;"></div> <div class="wave-bar" style="width: 4px; height: 40%; background: #a855f7; border-radius: 2px;"></div>
<div class="wave-bar" style="width: 4px; height: 70%; background: #c084fc; border-radius: 2px;"></div>
<div class="wave-bar" style="width: 4px; height: 30%; background: #a855f7; border-radius: 2px;"></div>
</div> </div>
<audio id="musicaAudioPlayer" controls style="width: 100%; margin-top: 2px;"></audio> <!-- Barra de Progresso Roxo Vibrante Interativa -->
<div id="musicaProgressBox" style="display: flex; flex-direction: column; gap: 6px; margin-top: 4px;">
<div id="musicaCustomProgressContainer" style="position: relative; width: 100%; height: 10px; background: rgba(255,255,255,0.12); border-radius: 6px; cursor: pointer; overflow: hidden; border: 1px solid rgba(168,85,247,0.3); box-shadow: inset 0 1px 3px rgba(0,0,0,0.5);">
<div id="musicaCustomProgressBar" style="position: absolute; left: 0; top: 0; bottom: 0; width: 0%; background: linear-gradient(90deg, #9333ea 0%, #a855f7 50%, #c084fc 100%); border-radius: 6px; box-shadow: 0 0 12px rgba(168,85,247,0.85); transition: width 0.1s linear;"></div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; font-size: 0.75rem; color: #c084fc; font-weight: 600;">
<span id="musicaCurrentTime">0:00</span>
<span style="font-size: 0.7rem; color: var(--text-secondary);">🟣 Barra de Reprodução Musical</span>
<span id="musicaDurationTime">0:00</span>
</div>
</div>
<audio id="musicaAudioPlayer" controls style="width: 100%; margin-top: 2px; accent-color: #a855f7;"></audio>
</div> </div>
<!-- ABAS DAS 3 VERSÕES --> <!-- ABAS DAS 3 VERSÕES -->
+14 -3
View File
@@ -4133,9 +4133,9 @@ code {
} }
@keyframes wave-pulse { @keyframes wave-pulse {
0% { height: 20%; background-color: #a855f7; } 0% { height: 20%; background-color: #9333ea; box-shadow: 0 0 4px #9333ea; }
50% { height: 95%; background-color: #ec4899; } 50% { height: 95%; background-color: #c084fc; box-shadow: 0 0 10px #c084fc; }
100% { height: 40%; background-color: #3b82f6; } 100% { height: 40%; background-color: #e879f9; box-shadow: 0 0 6px #e879f9; }
} }
@keyframes wave-pulse-poesia { @keyframes wave-pulse-poesia {
@@ -4150,6 +4150,17 @@ code {
100% { height: 40%; background-color: #059669; } 100% { height: 40%; background-color: #059669; }
} }
#musicaAudioPlayer {
accent-color: #a855f7 !important;
outline: none;
filter: contrast(1.15) saturate(1.4);
}
#musicaCustomProgressContainer:hover #musicaCustomProgressBar {
background: linear-gradient(90deg, #a855f7 0%, #c084fc 60%, #f472b6 100%) !important;
box-shadow: 0 0 16px rgba(192, 132, 252, 1) !important;
}
@media (max-width: 768px) { @media (max-width: 768px) {
#musicaLeftPanel, #musicaRightPanel, #musicaLeftPanel, #musicaRightPanel,
#poesiaLeftPanel, #poesiaRightPanel, #poesiaLeftPanel, #poesiaRightPanel,
+37 -20
View File
@@ -2533,25 +2533,38 @@ Retorne ESTRITAMENTE um objeto JSON válido (sem tags markdown extras) com a seg
let audioUrl = null; let audioUrl = null;
if (gerarAudio !== false) { if (gerarAudio !== false) {
try { try {
console.log('[Musicando] Iniciando geração de áudio musical...'); console.log('[Musicando] Iniciando geração de áudio musical cantado com instrumentos...');
const lyricsToSing = (musicaData.versaoPrincipal || musicaData.versaoCurta || tema).replace(/\(.*?\)/g, '').trim(); let rawSingable = (musicaData.versaoPrincipal || musicaData.versaoCurta || tema)
.replace(/\(.*?\)/g, '')
.replace(/[\[\]]/g, '')
.trim();
// Tentativa 1: MiniMax music-2.0 com timeout seguro de 20s (evita 502 de Gateway Proxy) // Estruturação métrica em versos e refrão para o motor de IA musical
let structuredLyrics = '';
const stanzas = rawSingable.split('\n\n').filter(s => s.trim().length > 0);
if (stanzas.length >= 2) {
structuredLyrics = `[verse]\n${stanzas[0].trim()}\n\n[chorus]\n${stanzas[1].trim()}` + (stanzas[2] ? `\n\n[verse]\n${stanzas[2].trim()}` : '');
} else {
structuredLyrics = `[verse]\n${rawSingable}`;
}
// Tentativa 1: MiniMax music-2.0 com timeout de 75s (tempo necessário para IA sintetizar voz cantada e instrumentos)
if (process.env.MINIMAX_API_KEY) { if (process.env.MINIMAX_API_KEY) {
try { try {
let voiceDesc = 'sweet native brazilian female voice, warm kindergarten teacher singing voice'; let voiceDesc = 'sweet melodious native brazilian female singing voice, cheerful acoustic kindergarten song';
if (voz === 'homem') { if (voz === 'homem') {
voiceDesc = 'gentle warm native brazilian male voice, friendly acoustic singer'; voiceDesc = 'gentle warm native brazilian male singing voice, friendly acoustic folk singer';
} else if (voz === 'crianca') { } else if (voz === 'crianca') {
voiceDesc = 'playful cute young brazilian child singing voice, energetic kid vocals'; voiceDesc = 'playful cute cheerful brazilian child singing voice, lively kindergarten vocals';
} }
const audioPrompt = `${ritmo || 'Cantiga de roda'}, ${instrumentos || 'acoustic guitar, percussion'}, ${voiceDesc}, educational children song, clear pt-BR pronunciation`; const audioPrompt = `${ritmo || 'Cantiga infantil alegre'}, instrumentos: ${instrumentos || 'violão acústico, flauta doce e percussão'}, ${voiceDesc}, Brazilian Portuguese sung vocals, clear harmonious melody, sweet kindergarten song, 128kbps studio mix`;
const minmBase = (process.env.MINIMAX_API_BASE || 'https://api.minimax.io/v1').replace(/\/v1\/?$/, ''); const minmBase = (process.env.MINIMAX_API_BASE || 'https://api.minimax.io/v1').replace(/\/v1\/?$/, '');
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 20000); const timeoutId = setTimeout(() => controller.abort(), 75000);
console.log('[Musicando] Chamando MiniMax music-2.0...');
const musicResp = await fetch(`${minmBase}/v1/music_generation`, { const musicResp = await fetch(`${minmBase}/v1/music_generation`, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -2561,7 +2574,7 @@ Retorne ESTRITAMENTE um objeto JSON válido (sem tags markdown extras) com a seg
body: JSON.stringify({ body: JSON.stringify({
model: 'music-2.0', model: 'music-2.0',
prompt: audioPrompt, prompt: audioPrompt,
lyrics: lyricsToSing.substring(0, 500), lyrics: structuredLyrics.substring(0, 600),
audio_setting: { sample_rate: 32000, bitrate: 128000, format: 'mp3' } audio_setting: { sample_rate: 32000, bitrate: 128000, format: 'mp3' }
}), }),
signal: controller.signal signal: controller.signal
@@ -2584,19 +2597,20 @@ Retorne ESTRITAMENTE um objeto JSON válido (sem tags markdown extras) com a seg
fs.writeFileSync(filePath, audioBuffer); fs.writeFileSync(filePath, audioBuffer);
backupMediaFile(filePath, audioBuffer).catch(() => {}); backupMediaFile(filePath, audioBuffer).catch(() => {});
audioUrl = `/generated-media/${fileName}`; audioUrl = `/generated-media/${fileName}`;
console.log('[Musicando] Áudio gerado com sucesso via MiniMax music-2.0!'); console.log('[Musicando] Áudio cantado gerado com sucesso via MiniMax music-2.0!');
} }
} else { } else {
console.warn('[Musicando] MiniMax áudio retornou status:', musicResp.status); const errTxt = await musicResp.text();
console.warn('[Musicando] MiniMax music respondeu status:', musicResp.status, errTxt);
} }
} catch (minimaxErr) { } catch (minimaxErr) {
console.warn('[Musicando] MiniMax music não respondeu a tempo (' + minimaxErr.message + '), acionando arranjo pedagógico vocal...'); console.warn('[Musicando] MiniMax music finalizou com fallback (' + minimaxErr.message + '), acionando arranjo harmônico instrumental...');
} }
} }
// Tentativa 2: Arranjo Pedagógico Vocal + Instrumental de Alta Velocidade (FFmpeg + MsEdgeTTS) // Tentativa 2: Arranjo Pedagógico Vocal + Base Instrumental Harmônica (FFmpeg + MsEdgeTTS)
if (!audioUrl) { if (!audioUrl) {
console.log('[Musicando] Sintetizando arranjo pedagógico vocal com melodia instrumental...'); console.log('[Musicando] Sintetizando arranjo pedagógico com base instrumental rica...');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const { exec } = require('child_process'); const { exec } = require('child_process');
@@ -2607,7 +2621,10 @@ Retorne ESTRITAMENTE um objeto JSON válido (sem tags markdown extras) com a seg
fs.mkdirSync(mediaDir, { recursive: true }); fs.mkdirSync(mediaDir, { recursive: true });
} }
const voiceName = (voz === 'homem') ? 'pt-BR-AntonioNeural' : 'pt-BR-FranciscaNeural'; let voiceName = 'pt-BR-FranciscaNeural';
if (voz === 'homem') voiceName = 'pt-BR-AntonioNeural';
else if (voz === 'crianca') voiceName = 'pt-BR-ThalitaNeural';
const tts = new MsEdgeTTS(); const tts = new MsEdgeTTS();
await tts.setMetadata(voiceName, OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3); await tts.setMetadata(voiceName, OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3);
@@ -2630,11 +2647,11 @@ Retorne ESTRITAMENTE um objeto JSON válido (sem tags markdown extras) com a seg
bgTrack = 'calma.mp3'; bgTrack = 'calma.mp3';
} else if (rLower.includes('circo') || rLower.includes('palha')) { } else if (rLower.includes('circo') || rLower.includes('palha')) {
bgTrack = 'circo.mp3'; bgTrack = 'circo.mp3';
} else if (rLower.includes('festa') || rLower.includes('balão') || rLower.includes('pipoca')) { } else if (rLower.includes('festa') || rLower.includes('balão') || rLower.includes('pipoca') || rLower.includes('dançante')) {
bgTrack = 'festa.mp3'; bgTrack = 'festa.mp3';
} else if (rLower.includes('bichinho') || rLower.includes('animal') || rLower.includes('fazenda')) { } else if (rLower.includes('bichinho') || rLower.includes('animal') || rLower.includes('fazenda') || rLower.includes('horta')) {
bgTrack = 'animais.mp3'; bgTrack = 'animais.mp3';
} else if (rLower.includes('brinquedo') || rLower.includes('fantasia')) { } else if (rLower.includes('brinquedo') || rLower.includes('fantasia') || rLower.includes('roda')) {
bgTrack = 'brinquedos.mp3'; bgTrack = 'brinquedos.mp3';
} }
@@ -2644,7 +2661,7 @@ Retorne ESTRITAMENTE um objeto JSON válido (sem tags markdown extras) com a seg
if (fs.existsSync(bgMusicPath) && fs.existsSync(voiceTempPath)) { if (fs.existsSync(bgMusicPath) && fs.existsSync(voiceTempPath)) {
await new Promise((resolve) => { await new Promise((resolve) => {
exec(`ffmpeg -y -i "${voiceTempPath}" -stream_loop -1 -i "${bgMusicPath}" -filter_complex "[0:a]volume=1.35[v];[1:a]volume=0.18[bg];[v][bg]amix=inputs=2:duration=first[aout]" -map "[aout]" -c:a libmp3lame -b:a 128k "${outFilePath}"`, () => { exec(`ffmpeg -y -i "${voiceTempPath}" -stream_loop -1 -i "${bgMusicPath}" -filter_complex "[0:a]volume=1.2[v];[1:a]volume=0.35[bg];[v][bg]amix=inputs=2:duration=first[aout]" -map "[aout]" -c:a libmp3lame -b:a 128k "${outFilePath}"`, () => {
try { if (fs.existsSync(voiceTempPath)) fs.unlinkSync(voiceTempPath); } catch (e) {} try { if (fs.existsSync(voiceTempPath)) fs.unlinkSync(voiceTempPath); } catch (e) {}
resolve(); resolve();
}); });
@@ -2657,7 +2674,7 @@ Retorne ESTRITAMENTE um objeto JSON válido (sem tags markdown extras) com a seg
const buf = fs.readFileSync(outFilePath); const buf = fs.readFileSync(outFilePath);
backupMediaFile(outFilePath, buf).catch(() => {}); backupMediaFile(outFilePath, buf).catch(() => {});
audioUrl = `/generated-media/${outFileName}`; audioUrl = `/generated-media/${outFileName}`;
console.log('[Musicando] Arranjo pedagógico concluído com sucesso!'); console.log('[Musicando] Arranjo pedagógico instrumental concluído com sucesso!');
} }
} }
} catch (audioErr) { } catch (audioErr) {