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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user