Correcao do Musicando Ideias: timeout de gateway 502, arranjo vocal/instrumental sintetizado e tratamento de erro JSON

This commit is contained in:
2026-09-01 02:24:52 +00:00
parent 10cca214e0
commit ee1cd3bbe5
2 changed files with 130 additions and 46 deletions
+5 -2
View File
@@ -7610,11 +7610,14 @@ window.toggleCustomAudio = (btn) => {
turmaId: musicaTurmaSelect?.value || null, turmaId: musicaTurmaSelect?.value || null,
gerarAudio: true gerarAudio: true
}) })
}); if (!res.ok) {
const errMsg = await safeExtractError(res, 'Erro ao compor música');
throw new Error(errMsg);
}
const data = await res.json(); const data = await res.json();
if (res.ok && data.success) { if (data.success) {
activeMusicaData = { activeMusicaData = {
titulo: data.titulo || `Música: ${tema}`, titulo: data.titulo || `Música: ${tema}`,
versaoPrincipal: data.versaoPrincipal, versaoPrincipal: data.versaoPrincipal,
+90 -9
View File
@@ -2529,11 +2529,16 @@ Retorne ESTRITAMENTE um objeto JSON válido (sem tags markdown extras) com a seg
}; };
} }
// 4. GERAÇÃO DE ÁUDIO CANTADO (MiniMax music-2.0) // 4. GERAÇÃO DE ÁUDIO CANTADO / INSTRUMENTALIZADO
let audioUrl = null; let audioUrl = null;
if (gerarAudio !== false && process.env.MINIMAX_API_KEY) { if (gerarAudio !== false) {
try {
console.log('[Musicando] Iniciando geração de áudio musical...');
const lyricsToSing = (musicaData.versaoPrincipal || musicaData.versaoCurta || tema).replace(/\(.*?\)/g, '').trim();
// Tentativa 1: MiniMax music-2.0 com timeout seguro de 20s (evita 502 de Gateway Proxy)
if (process.env.MINIMAX_API_KEY) {
try { try {
console.log('[Musicando] Gerando áudio cantado com MiniMax music-2.0...');
let voiceDesc = 'sweet native brazilian female voice, warm kindergarten teacher singing voice'; let voiceDesc = 'sweet native brazilian female voice, warm kindergarten teacher singing voice';
if (voz === 'homem') { if (voz === 'homem') {
voiceDesc = 'gentle warm native brazilian male voice, friendly acoustic singer'; voiceDesc = 'gentle warm native brazilian male voice, friendly acoustic singer';
@@ -2542,9 +2547,11 @@ Retorne ESTRITAMENTE um objeto JSON válido (sem tags markdown extras) com a seg
} }
const audioPrompt = `${ritmo || 'Cantiga de roda'}, ${instrumentos || 'acoustic guitar, percussion'}, ${voiceDesc}, educational children song, clear pt-BR pronunciation`; const audioPrompt = `${ritmo || 'Cantiga de roda'}, ${instrumentos || 'acoustic guitar, percussion'}, ${voiceDesc}, educational children song, clear pt-BR pronunciation`;
const lyricsToSing = (musicaData.versaoPrincipal || musicaData.versaoCurta || tema).replace(/\(.*?\)/g, '').trim();
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 timeoutId = setTimeout(() => controller.abort(), 20000);
const musicResp = await fetch(`${minmBase}/v1/music_generation`, { const musicResp = await fetch(`${minmBase}/v1/music_generation`, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -2556,8 +2563,10 @@ Retorne ESTRITAMENTE um objeto JSON válido (sem tags markdown extras) com a seg
prompt: audioPrompt, prompt: audioPrompt,
lyrics: lyricsToSing.substring(0, 500), lyrics: lyricsToSing.substring(0, 500),
audio_setting: { sample_rate: 32000, bitrate: 128000, format: 'mp3' } audio_setting: { sample_rate: 32000, bitrate: 128000, format: 'mp3' }
}) }),
signal: controller.signal
}); });
clearTimeout(timeoutId);
if (musicResp.ok) { if (musicResp.ok) {
const mData = await musicResp.json(); const mData = await musicResp.json();
@@ -2573,14 +2582,86 @@ Retorne ESTRITAMENTE um objeto JSON válido (sem tags markdown extras) com a seg
const filePath = path.join(mediaDir, fileName); const filePath = path.join(mediaDir, fileName);
const audioBuffer = Buffer.from(audioHex, 'hex'); const audioBuffer = Buffer.from(audioHex, 'hex');
fs.writeFileSync(filePath, audioBuffer); fs.writeFileSync(filePath, audioBuffer);
await backupMediaFile(filePath, audioBuffer); backupMediaFile(filePath, audioBuffer).catch(() => {});
audioUrl = `/generated-media/${fileName}`; audioUrl = `/generated-media/${fileName}`;
console.log('[Musicando] Áudio gerado com sucesso via MiniMax music-2.0!');
} }
} else { } else {
console.warn('[Musicando] Falha MiniMax áudio:', await musicResp.text()); console.warn('[Musicando] MiniMax áudio retornou status:', musicResp.status);
}
} catch (minimaxErr) {
console.warn('[Musicando] MiniMax music não respondeu a tempo (' + minimaxErr.message + '), acionando arranjo pedagógico vocal...');
}
}
// Tentativa 2: Arranjo Pedagógico Vocal + Instrumental de Alta Velocidade (FFmpeg + MsEdgeTTS)
if (!audioUrl) {
console.log('[Musicando] Sintetizando arranjo pedagógico vocal com melodia instrumental...');
const fs = require('fs');
const path = require('path');
const { exec } = require('child_process');
const { MsEdgeTTS, OUTPUT_FORMAT } = require('msedge-tts');
const mediaDir = path.join(__dirname, 'public', 'generated-media');
if (!fs.existsSync(mediaDir)) {
fs.mkdirSync(mediaDir, { recursive: true });
}
const voiceName = (voz === 'homem') ? 'pt-BR-AntonioNeural' : 'pt-BR-FranciscaNeural';
const tts = new MsEdgeTTS();
await tts.setMetadata(voiceName, OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3);
const rawLyrics = (musicaData.versaoPrincipal || musicaData.versaoCurta || tema).replace(/[\(\)]/g, '');
const voiceTempPath = path.join(mediaDir, `voice_temp_${Date.now()}_${Math.floor(Math.random()*1000)}.mp3`);
const { audioStream } = tts.toStream(rawLyrics);
await new Promise((resolve, reject) => {
const ws = fs.createWriteStream(voiceTempPath);
audioStream.pipe(ws);
ws.on('finish', resolve);
ws.on('error', reject);
audioStream.on('error', reject);
});
// Selecionar trilha instrumental harmônica de acordo com o ritmo
let bgTrack = 'alegre.mp3';
const rLower = (ritmo || '').toLowerCase();
if (rLower.includes('calm') || rLower.includes('ninar') || rLower.includes('suave') || rLower.includes('berço')) {
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')) {
bgTrack = 'festa.mp3';
} else if (rLower.includes('bichinho') || rLower.includes('animal') || rLower.includes('fazenda')) {
bgTrack = 'animais.mp3';
} else if (rLower.includes('brinquedo') || rLower.includes('fantasia')) {
bgTrack = 'brinquedos.mp3';
}
const bgMusicPath = path.join(__dirname, 'public', 'assets', 'audio', bgTrack);
const outFileName = `musica_${Date.now()}_${Math.floor(Math.random()*1000)}.mp3`;
const outFilePath = path.join(mediaDir, outFileName);
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}"`, () => {
try { if (fs.existsSync(voiceTempPath)) fs.unlinkSync(voiceTempPath); } catch (e) {}
resolve();
});
});
} else if (fs.existsSync(voiceTempPath)) {
fs.renameSync(voiceTempPath, outFilePath);
}
if (fs.existsSync(outFilePath)) {
const buf = fs.readFileSync(outFilePath);
backupMediaFile(outFilePath, buf).catch(() => {});
audioUrl = `/generated-media/${outFileName}`;
console.log('[Musicando] Arranjo pedagógico concluído com sucesso!');
}
} }
} catch (audioErr) { } catch (audioErr) {
console.error('[Musicando] Erro ao gerar áudio MiniMax:', audioErr.message); console.error('[Musicando] Erro na síntese de áudio:', audioErr.message);
} }
} }