fix: Migração definitiva da síntese musical para motor de arranjo instrumental harmônico com masterização FFmpeg e eliminação de erros de API descontinuada
This commit is contained in:
Binary file not shown.
@@ -334,6 +334,98 @@ app.post('/api/tts', requireAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// HELPER: Síntese Musical Pedagógica com Voz Melódica & Arranjo Instrumental
|
||||||
|
// ============================================================================
|
||||||
|
async function synthesizePedagogicalSong({ lyrics, ritmo = 'Cantiga de Roda', tema = '', voz = 'mulher', instrumentos = '' }) {
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Limpar a letra de marcações extras e gestos entre parênteses
|
||||||
|
const cleanLyrics = (lyrics || tema || 'Vamos cantar e aprender!')
|
||||||
|
.replace(/\(.*?\)/g, '')
|
||||||
|
.replace(/\[.*?\]/g, '')
|
||||||
|
.replace(/[#*]/g, '')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
// 2. Determinar voz da cantiga
|
||||||
|
let voiceName = 'pt-BR-FranciscaNeural';
|
||||||
|
if (voz === 'homem') {
|
||||||
|
voiceName = 'pt-BR-AntonioNeural';
|
||||||
|
} else if (voz === 'crianca' || voz === 'criancas') {
|
||||||
|
voiceName = 'pt-BR-ThalitaNeural';
|
||||||
|
}
|
||||||
|
|
||||||
|
const tts = new MsEdgeTTS();
|
||||||
|
await tts.setMetadata(voiceName, OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3);
|
||||||
|
|
||||||
|
const voiceTempPath = path.join(mediaDir, `voice_temp_${Date.now()}_${Math.floor(Math.random() * 10000)}.mp3`);
|
||||||
|
|
||||||
|
// 3. Sintetizar stream vocal da cantiga
|
||||||
|
const { audioStream } = tts.toStream(cleanLyrics);
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Seleção da melhor trilha instrumental temática
|
||||||
|
let bgTrack = 'alegre.mp3';
|
||||||
|
const rLower = (ritmo + ' ' + tema + ' ' + instrumentos).toLowerCase();
|
||||||
|
if (rLower.includes('calm') || rLower.includes('ninar') || rLower.includes('suave') || rLower.includes('berço') || rLower.includes('dormir') || rLower.includes('naninha')) {
|
||||||
|
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') || rLower.includes('dançante') || rLower.includes('pop')) {
|
||||||
|
bgTrack = 'festa.mp3';
|
||||||
|
} else if (rLower.includes('bichinho') || rLower.includes('animal') || rLower.includes('fazenda') || rLower.includes('horta') || rLower.includes('planta')) {
|
||||||
|
bgTrack = 'animais.mp3';
|
||||||
|
} else if (rLower.includes('brinquedo') || rLower.includes('fantasia') || rLower.includes('bonec')) {
|
||||||
|
bgTrack = 'brinquedos.mp3';
|
||||||
|
} else if (rLower.includes('amig') || rLower.includes('escola') || rLower.includes('roda')) {
|
||||||
|
bgTrack = 'amizade.mp3';
|
||||||
|
}
|
||||||
|
|
||||||
|
let bgMusicPath = path.join(__dirname, 'public', 'assets', 'audio', bgTrack);
|
||||||
|
if (!fs.existsSync(bgMusicPath)) {
|
||||||
|
bgMusicPath = path.join(__dirname, 'public', 'assets', 'audio', 'alegre.mp3');
|
||||||
|
}
|
||||||
|
|
||||||
|
const outFileName = `musica_${Date.now()}_${Math.floor(Math.random() * 10000)}.mp3`;
|
||||||
|
const outFilePath = path.join(mediaDir, outFileName);
|
||||||
|
|
||||||
|
// 5. Mixagem e Masterização com FFmpeg (Vocal em destaque com reverb acústico + Trilha instrumental viva 0.48)
|
||||||
|
if (fs.existsSync(bgMusicPath) && fs.existsSync(voiceTempPath)) {
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
const ffmpegCmd = `ffmpeg -y -i "${voiceTempPath}" -stream_loop -1 -i "${bgMusicPath}" -filter_complex "[0:a]volume=1.25,highpass=f=100,aecho=0.8:0.7:30|50:0.3|0.2[v];[1:a]volume=0.48[bg];[v][bg]amix=inputs=2:duration=first:dropout_transition=2[aout]" -map "[aout]" -c:a libmp3lame -b:a 128k "${outFilePath}"`;
|
||||||
|
exec(ffmpegCmd, (err) => {
|
||||||
|
if (err) console.warn('[SynthesizeSong] Erro FFmpeg mix:', err.message);
|
||||||
|
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(() => {});
|
||||||
|
return `/generated-media/${outFileName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Rota POST para gerar música customizada (Estúdio Musical)
|
// Rota POST para gerar música customizada (Estúdio Musical)
|
||||||
app.post('/api/music/generate', requireAuth, async (req, res) => {
|
app.post('/api/music/generate', requireAuth, async (req, res) => {
|
||||||
const { tema, voz, ritmo, duracao } = req.body;
|
const { tema, voz, ritmo, duracao } = req.body;
|
||||||
@@ -342,28 +434,7 @@ app.post('/api/music/generate', requireAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Determinar o prompt de estilo baseado nos inputs
|
// 1. Determinar o tamanho das letras baseado na duração (duracao)
|
||||||
let voiceDesc = 'young female vocals, singing in clear native brazilian portuguese accent, no european accent, sweet and warm voice';
|
|
||||||
if (voz === 'homem') {
|
|
||||||
voiceDesc = 'male vocals, singing in clear native brazilian portuguese accent, warm and gentle singing voice';
|
|
||||||
} else if (voz === 'crianca') {
|
|
||||||
voiceDesc = 'real young kid vocals, little child singing, boy or girl child voice, playful children voice, native brazilian portuguese kid accent, definitely no adult voices';
|
|
||||||
}
|
|
||||||
|
|
||||||
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, singing exclusively in native Brazilian Portuguese (PT-BR) accent, authentic Brazilian vocals, clear pronunciation`;
|
|
||||||
|
|
||||||
// 2. Determinar o tamanho das letras baseado na duração (duracao)
|
|
||||||
let lengthInstructions = 'curta, de apenas 6 a 8 versos';
|
let lengthInstructions = 'curta, de apenas 6 a 8 versos';
|
||||||
if (duracao === '1min') {
|
if (duracao === '1min') {
|
||||||
lengthInstructions = 'média, de 12 a 16 versos (com verso 1, refrão, verso 2, refrão)';
|
lengthInstructions = 'média, de 12 a 16 versos (com verso 1, refrão, verso 2, refrão)';
|
||||||
@@ -371,7 +442,7 @@ app.post('/api/music/generate', requireAuth, async (req, res) => {
|
|||||||
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)';
|
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
|
// 2. Chamar a IA para compor a letra
|
||||||
const lyricsPrompt = `Gere uma letra de cantiga escolar infantil sobre o tema: "${tema}".
|
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 letra deve ser escrita para o público infantil (linguagem lúdica, alegre e educativa).
|
||||||
A estrutura de versos deve ser: ${lengthInstructions}.
|
A estrutura de versos deve ser: ${lengthInstructions}.
|
||||||
@@ -389,52 +460,21 @@ Retorne APENAS a letra formatada em versos com quebras de linha. Não adicione n
|
|||||||
throw new Error('Falha ao compor a letra da música.');
|
throw new Error('Falha ao compor a letra da música.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Chamar a API da MiniMax para gerar a música (canto + melodia)
|
// 3. Sintetizar a canção com arranjo instrumental completo e vocal
|
||||||
const minmBase = (process.env.MINIMAX_API_BASE || 'https://api.minimax.io/v1').replace(/\/v1\/?$/, '');
|
console.log('[Estúdio Musical] Sintetizando música infantil com arranjo instrumental...');
|
||||||
const musicResp = await fetch(`${minmBase}/v1/music_generation`, {
|
const audioUrl = await synthesizePedagogicalSong({
|
||||||
method: 'POST',
|
lyrics,
|
||||||
headers: {
|
ritmo: ritmo || 'Cantiga de Roda',
|
||||||
'Authorization': `Bearer ${process.env.MINIMAX_API_KEY}`,
|
tema,
|
||||||
'Content-Type': 'application/json'
|
voz: voz || 'mulher',
|
||||||
},
|
instrumentos: 'Violão acústico, flauta e percussão'
|
||||||
body: JSON.stringify({
|
|
||||||
model: 'music-2.0',
|
|
||||||
prompt: stylePrompt,
|
|
||||||
lyrics: lyrics,
|
|
||||||
audio_setting: { sample_rate: 32000, bitrate: 128000, format: 'mp3' }
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!musicResp.ok) {
|
// 4. Salvar no Banco
|
||||||
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);
|
|
||||||
const audioBuffer = Buffer.from(audioHex, 'hex');
|
|
||||||
fs.writeFileSync(filePath, audioBuffer);
|
|
||||||
await backupMediaFile(filePath, audioBuffer);
|
|
||||||
const audioUrl = `/generated-media/${fileName}`;
|
|
||||||
|
|
||||||
// 6. Retornar dados para o frontend
|
|
||||||
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
|
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
|
||||||
const dbRes = await dbPool.query(
|
const dbRes = await dbPool.query(
|
||||||
`INSERT INTO escola.estudio_musicas (usuario_id, voz, ritmo, tema, letra, audio_url) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`,
|
`INSERT INTO escola.estudio_musicas (usuario_id, voz, ritmo, tema, letra, audio_url) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`,
|
||||||
[usuarioId, voz, ritmo, tema, lyrics, audioUrl]
|
[usuarioId, voz || 'mulher', ritmo || 'roda', tema, lyrics, audioUrl]
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -2529,154 +2569,19 @@ Retorne ESTRITAMENTE um objeto JSON válido (sem tags markdown extras) com a seg
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. GERAÇÃO DE ÁUDIO CANTADO / INSTRUMENTALIZADO
|
// 4. GERAÇÃO DE ÁUDIO CANTADO COM ARRANJO INSTRUMENTAL COMPLETO
|
||||||
let audioUrl = null;
|
let audioUrl = null;
|
||||||
if (gerarAudio !== false) {
|
if (gerarAudio !== false) {
|
||||||
try {
|
try {
|
||||||
console.log('[Musicando] Iniciando geração de áudio musical cantado com instrumentos...');
|
console.log('[Musicando] Sintetizando música pedagógica com arranjo instrumental rico...');
|
||||||
let rawSingable = (musicaData.versaoPrincipal || musicaData.versaoCurta || tema)
|
const lyricsToSing = musicaData.versaoPrincipal || musicaData.versaoCurta || tema;
|
||||||
.replace(/\(.*?\)/g, '')
|
audioUrl = await synthesizePedagogicalSong({
|
||||||
.replace(/[\[\]]/g, '')
|
lyrics: lyricsToSing,
|
||||||
.trim();
|
ritmo: ritmo || 'Cantiga de Roda',
|
||||||
|
tema: tema,
|
||||||
// Estruturação métrica em versos e refrão para o motor de IA musical
|
voz: voz || 'mulher',
|
||||||
let structuredLyrics = '';
|
instrumentos: instrumentos || 'Violão, Pandeiro e Flauta'
|
||||||
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 melodious native brazilian female singing voice, cheerful acoustic kindergarten song';
|
|
||||||
if (voz === 'homem') {
|
|
||||||
voiceDesc = 'gentle warm native brazilian male singing voice, friendly acoustic folk singer';
|
|
||||||
} else if (voz === 'crianca') {
|
|
||||||
voiceDesc = 'playful cute cheerful brazilian child singing voice, lively kindergarten vocals';
|
|
||||||
}
|
|
||||||
|
|
||||||
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(), 75000);
|
|
||||||
|
|
||||||
console.log('[Musicando] Chamando MiniMax music-2.0...');
|
|
||||||
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: audioPrompt,
|
|
||||||
lyrics: structuredLyrics.substring(0, 600),
|
|
||||||
audio_setting: { sample_rate: 32000, bitrate: 128000, format: 'mp3' }
|
|
||||||
}),
|
|
||||||
signal: controller.signal
|
|
||||||
});
|
});
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
if (musicResp.ok) {
|
|
||||||
const mData = await musicResp.json();
|
|
||||||
const audioHex = mData.data?.audio;
|
|
||||||
if (audioHex) {
|
|
||||||
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 = `musica_${Date.now()}.mp3`;
|
|
||||||
const filePath = path.join(mediaDir, fileName);
|
|
||||||
const audioBuffer = Buffer.from(audioHex, 'hex');
|
|
||||||
fs.writeFileSync(filePath, audioBuffer);
|
|
||||||
backupMediaFile(filePath, audioBuffer).catch(() => {});
|
|
||||||
audioUrl = `/generated-media/${fileName}`;
|
|
||||||
console.log('[Musicando] Áudio cantado gerado com sucesso via MiniMax music-2.0!');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const errTxt = await musicResp.text();
|
|
||||||
console.warn('[Musicando] MiniMax music respondeu status:', musicResp.status, errTxt);
|
|
||||||
}
|
|
||||||
} catch (minimaxErr) {
|
|
||||||
console.warn('[Musicando] MiniMax music finalizou com fallback (' + minimaxErr.message + '), acionando arranjo harmônico instrumental...');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tentativa 2: Arranjo Pedagógico Vocal + Base Instrumental Harmônica (FFmpeg + MsEdgeTTS)
|
|
||||||
if (!audioUrl) {
|
|
||||||
console.log('[Musicando] Sintetizando arranjo pedagógico com base instrumental rica...');
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
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') || rLower.includes('dançante')) {
|
|
||||||
bgTrack = 'festa.mp3';
|
|
||||||
} 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') || rLower.includes('roda')) {
|
|
||||||
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.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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} 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 instrumental concluído com sucesso!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (audioErr) {
|
} catch (audioErr) {
|
||||||
console.error('[Musicando] Erro na síntese de áudio:', audioErr.message);
|
console.error('[Musicando] Erro na síntese de áudio:', audioErr.message);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user