Feat/Fix: Adiciona histórico com exclusão para o Estúdio Musical e melhora prompt de voz (PT-BR infantil nativo)

This commit is contained in:
2026-06-05 19:01:18 +00:00
parent 2b697cacae
commit 4ba98c1545
4 changed files with 168 additions and 7 deletions
+83
View File
@@ -3698,6 +3698,89 @@ const initApp = () => {
});
}
// =====================================
// HISTÓRICO DO ESTÚDIO MUSICAL
// =====================================
const btnOpenEstudioHistory = document.getElementById('btnOpenEstudioHistory');
const estudioHistoryModal = document.getElementById('estudioHistoryModal');
const btnCloseEstudioHistory = document.getElementById('btnCloseEstudioHistory');
const estudioHistoryList = document.getElementById('estudioHistoryList');
if (btnOpenEstudioHistory) {
btnOpenEstudioHistory.addEventListener('click', async () => {
estudioHistoryModal.style.display = 'flex';
estudioHistoryList.innerHTML = '<div style="text-align: center; color: var(--text-secondary); padding: 20px;">Carregando histórico...</div>';
try {
const res = await fetch('/api/music/list');
const data = await res.json();
estudioHistoryList.innerHTML = '';
if (data.length === 0) {
estudioHistoryList.innerHTML = '<div style="text-align: center; color: var(--text-secondary); padding: 20px;">Nenhuma canção salva ainda no estúdio.</div>';
return;
}
data.forEach(item => {
const div = document.createElement('div');
div.style.background = 'var(--bg-tertiary)';
div.style.padding = '12px 15px';
div.style.borderRadius = '8px';
div.style.display = 'flex';
div.style.justifyContent = 'space-between';
div.style.alignItems = 'center';
div.innerHTML = `
<div style="flex: 1; margin-right: 15px;">
<div style="font-weight: 600; font-size: 0.95rem; color: var(--text-primary);">🎵 ${item.tema || 'Música'}</div>
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-top: 4px;">Voz: ${item.voz} | Ritmo: ${item.ritmo}</div>
<div style="font-size: 0.75rem; color: var(--text-secondary); margin-top: 4px;">${new Date(item.created_at).toLocaleString()}</div>
</div>
<div style="display: flex; gap: 8px;">
<button class="btn-ver-estudio btn-secondary" data-id="${item.id}" style="padding: 6px 12px; font-size: 0.85rem; border-radius: 6px;">Ver/Ouvir</button>
<button class="btn-del-estudio btn-secondary" data-id="${item.id}" style="padding: 6px 12px; font-size: 0.85rem; border-radius: 6px; color: #ef4444; border-color: rgba(239, 68, 68, 0.3);">🗑</button>
</div>
`;
estudioHistoryList.appendChild(div);
});
document.querySelectorAll('.btn-ver-estudio').forEach(btn => {
btn.addEventListener('click', async (e) => {
const id = e.target.dataset.id;
try {
const res = await fetch(`/api/music/${id}`);
const itemData = await res.json();
musicStudioLyricsArea.textContent = itemData.letra;
musicStudioAudioPlayer.src = itemData.audio_url;
musicStudioDownloadBtn.href = itemData.audio_url;
musicResultArea.style.display = 'flex';
estudioHistoryModal.style.display = 'none';
} catch (err) { alert('Erro ao carregar música do estúdio.'); }
});
});
document.querySelectorAll('.btn-del-estudio').forEach(btn => {
btn.addEventListener('click', async (e) => {
if(!confirm('Tem certeza que deseja apagar esta canção?')) return;
const id = e.target.dataset.id;
try {
await fetch(`/api/music/${id}`, { method: 'DELETE' });
e.target.closest('div[style*="var(--bg-tertiary)"]').remove();
} catch (err) { alert('Erro ao deletar.'); }
});
});
} catch (err) {
estudioHistoryList.innerHTML = '<div style="color: #ef4444; padding: 20px;">Erro ao carregar histórico.</div>';
}
});
}
if (btnCloseEstudioHistory) {
btnCloseEstudioHistory.addEventListener('click', () => {
estudioHistoryModal.style.display = 'none';
});
}
// ============================================================
// FÁBRICA DE QUADRINHOS (Criação de histórias visuais pedagógicas)
// ============================================================
+21 -1
View File
@@ -655,7 +655,12 @@
<div class="settings-modal-content" style="max-width: 600px; width: 95%; max-height: 90vh; display: flex; flex-direction: column;">
<div class="settings-modal-header" style="background: var(--bg-secondary); border-bottom: 1px solid var(--border-light);">
<h3 style="display: flex; align-items: center; gap: 8px; font-family: 'Outfit', sans-serif;">🎵 Estúdio Musical PedaGog</h3>
<button id="btnCloseEstudioModal" class="btn-close-modal" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.5rem;">&times;</button>
<div style="display: flex; gap: 10px; align-items: center;">
<button id="btnOpenEstudioHistory" class="btn-secondary" style="padding: 6px 12px; font-size: 0.85rem; border-radius: 6px; display: flex; align-items: center; gap: 6px; background: rgba(168, 85, 247, 0.1); color: #c084fc; border: 1px solid rgba(168, 85, 247, 0.2);">
🕒 Histórico
</button>
<button id="btnCloseEstudioModal" class="btn-close-modal" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.5rem;">&times;</button>
</div>
</div>
<div class="settings-modal-body" style="padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 18px; flex: 1;">
@@ -736,6 +741,21 @@
</div>
</div>
<!-- Modal: Histórico Estúdio Musical -->
<div id="estudioHistoryModal" class="settings-modal" style="display: none; z-index: 20000;">
<div class="settings-modal-content" style="max-width: 600px; width: 95%; max-height: 80vh; display: flex; flex-direction: column;">
<div class="settings-modal-header" style="background: var(--bg-secondary); border-bottom: 1px solid var(--border-light);">
<h3 style="display: flex; align-items: center; gap: 8px; font-family: 'Outfit', sans-serif;">🕒 Histórico do Estúdio</h3>
<button id="btnCloseEstudioHistory" class="btn-close-modal" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.5rem;">&times;</button>
</div>
<div class="settings-modal-body" style="padding: 20px; overflow-y: auto; flex: 1;">
<div id="estudioHistoryList" style="display: flex; flex-direction: column; gap: 10px;">
<!-- Preenchido dinamicamente -->
</div>
</div>
</div>
</div>
<!-- Modal: Estúdio de Poesia -->
<!-- ========================================== -->
<!-- MODAL: MUSICANDO IDEIAS -->
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE_NAME = 'camila-ai-v5';
const CACHE_NAME = 'camila-ai-v6';
const urlsToCache = [
'/',
'/index.html',
+63 -5
View File
@@ -220,11 +220,11 @@ app.post('/api/music/generate', requireAuth, async (req, res) => {
try {
// 1. Determinar o prompt de estilo baseado nos inputs
let voiceDesc = 'female vocals, clear and sweet singing voice';
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, warm and gentle singing voice';
voiceDesc = 'male vocals, singing in clear native brazilian portuguese accent, warm and gentle singing voice';
} else if (voz === 'crianca') {
voiceDesc = 'children vocals, sweet child singing voice';
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';
@@ -238,7 +238,7 @@ app.post('/api/music/generate', requireAuth, async (req, res) => {
rhythmDesc = 'marching children rhythm, playful snare drums, acoustic guitar, brass accent';
}
const stylePrompt = `${rhythmDesc}, ${voiceDesc}, educational children song style, singing in Brazilian Portuguese, Brazilian Portuguese vocals, clear Brazilian pronunciation, 100 BPM`;
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';
@@ -306,9 +306,16 @@ Retorne APENAS a letra formatada em versos com quebras de linha. Não adicione n
const audioUrl = `/generated-media/${fileName}`;
// 6. Retornar dados para o frontend
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
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`,
[usuarioId, voz, ritmo, tema, lyrics, audioUrl]
);
res.json({
success: true,
lyrics: lyrics,
id: dbRes.rows[0].id,
lyrics: lyrics,
audioUrl: audioUrl
});
@@ -318,6 +325,45 @@ Retorne APENAS a letra formatada em versos com quebras de linha. Não adicione n
}
});
// ============================================================
// ROTAS DE HISTÓRICO DO ESTÚDIO MUSICAL
// ============================================================
app.get('/api/music/list', requireAuth, async (req, res) => {
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
try {
const dbRes = await dbPool.query(`SELECT id, voz, ritmo, tema, created_at FROM escola.estudio_musicas WHERE usuario_id = $1 ORDER BY created_at DESC`, [usuarioId]);
res.json(dbRes.rows);
} catch (err) { res.status(500).json({ error: 'Erro ao listar histórico.' }); }
});
app.get('/api/music/:id', requireAuth, async (req, res) => {
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
try {
const dbRes = await dbPool.query(`SELECT * FROM escola.estudio_musicas WHERE id = $1 AND usuario_id = $2`, [req.params.id, usuarioId]);
if (dbRes.rows.length === 0) return res.status(404).json({ error: 'Não encontrada.' });
res.json(dbRes.rows[0]);
} catch (err) { res.status(500).json({ error: 'Erro ao buscar.' }); }
});
app.delete('/api/music/:id', requireAuth, async (req, res) => {
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
try {
// Apagar também o arquivo físico se quiser
const fileRes = await dbPool.query(`SELECT audio_url FROM escola.estudio_musicas WHERE id = $1 AND usuario_id = $2`, [req.params.id, usuarioId]);
if (fileRes.rows.length > 0 && fileRes.rows[0].audio_url) {
const fs = require('fs');
const path = require('path');
const filePath = path.join(__dirname, 'public', fileRes.rows[0].audio_url);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
}
await dbPool.query(`DELETE FROM escola.estudio_musicas WHERE id = $1 AND usuario_id = $2`, [req.params.id, usuarioId]);
res.json({ success: true });
} catch (err) { res.status(500).json({ error: 'Erro ao deletar.' }); }
});
// ============================================================
// ROTAS DO ESTÚDIO: FÁBRICA DE QUADRINHOS
// ============================================================
@@ -1770,6 +1816,18 @@ async function initDatabase() {
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Estúdio Musical (Áudio MiniMax)
CREATE TABLE IF NOT EXISTS escola.estudio_musicas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
usuario_id UUID NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000',
voz VARCHAR(50),
ritmo VARCHAR(100),
tema TEXT,
letra TEXT,
audio_url TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Histórias (Inventando Histórias)
CREATE TABLE IF NOT EXISTS escola.historias (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),