diff --git a/public/app.js b/public/app.js index 3cd6ddc..4828c4c 100644 --- a/public/app.js +++ b/public/app.js @@ -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) { musicaAudioPlayer.addEventListener('play', () => { const bars = document.querySelectorAll('#musicaWaveformVisualizer .wave-bar'); @@ -7601,6 +7614,30 @@ window.toggleCustomAudio = (btn) => { const bars = document.querySelectorAll('#musicaWaveformVisualizer .wave-bar'); 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) --- diff --git a/public/index.html b/public/index.html index 7fef0a4..a83983f 100644 --- a/public/index.html +++ b/public/index.html @@ -1138,20 +1138,34 @@ -
+
-
-
+
+
-
-
+
+
-
-
+
+
+
+
- + +
+
+
+
+
+ 0:00 + 🟣 Barra de Reprodução Musical + 0:00 +
+
+ +
diff --git a/public/style.css b/public/style.css index 19a1d5a..5c06bc7 100644 --- a/public/style.css +++ b/public/style.css @@ -4133,9 +4133,9 @@ code { } @keyframes wave-pulse { - 0% { height: 20%; background-color: #a855f7; } - 50% { height: 95%; background-color: #ec4899; } - 100% { height: 40%; background-color: #3b82f6; } + 0% { height: 20%; background-color: #9333ea; box-shadow: 0 0 4px #9333ea; } + 50% { height: 95%; background-color: #c084fc; box-shadow: 0 0 10px #c084fc; } + 100% { height: 40%; background-color: #e879f9; box-shadow: 0 0 6px #e879f9; } } @keyframes wave-pulse-poesia { @@ -4150,6 +4150,17 @@ code { 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) { #musicaLeftPanel, #musicaRightPanel, #poesiaLeftPanel, #poesiaRightPanel, diff --git a/server.js b/server.js index 896ba47..39973a2 100644 --- a/server.js +++ b/server.js @@ -2533,25 +2533,38 @@ Retorne ESTRITAMENTE um objeto JSON válido (sem tags markdown extras) com a seg let audioUrl = null; if (gerarAudio !== false) { try { - console.log('[Musicando] Iniciando geração de áudio musical...'); - const lyricsToSing = (musicaData.versaoPrincipal || musicaData.versaoCurta || tema).replace(/\(.*?\)/g, '').trim(); + console.log('[Musicando] Iniciando geração de áudio musical cantado com instrumentos...'); + 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) { 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') { - 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') { - 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 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`, { method: 'POST', headers: { @@ -2561,7 +2574,7 @@ Retorne ESTRITAMENTE um objeto JSON válido (sem tags markdown extras) com a seg body: JSON.stringify({ model: 'music-2.0', prompt: audioPrompt, - lyrics: lyricsToSing.substring(0, 500), + lyrics: structuredLyrics.substring(0, 600), audio_setting: { sample_rate: 32000, bitrate: 128000, format: 'mp3' } }), 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); backupMediaFile(filePath, audioBuffer).catch(() => {}); 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 { - 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) { - 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) { - 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 path = require('path'); 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 }); } - 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(); 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'; } else if (rLower.includes('circo') || rLower.includes('palha')) { 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'; - } 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'; - } else if (rLower.includes('brinquedo') || rLower.includes('fantasia')) { + } else if (rLower.includes('brinquedo') || rLower.includes('fantasia') || rLower.includes('roda')) { 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)) { 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) {} resolve(); }); @@ -2657,7 +2674,7 @@ Retorne ESTRITAMENTE um objeto JSON válido (sem tags markdown extras) com a seg const buf = fs.readFileSync(outFilePath); backupMediaFile(outFilePath, buf).catch(() => {}); 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) {