Feature: Estúdio de Poesias profissional com dual-stage engine, analise de metricas literarias, guia fonetico, audio TTS narrado, player com equalizer e exportacao PDF/TXT

This commit is contained in:
2026-08-31 11:21:33 +00:00
parent 88d7163b9f
commit 3931af93db
4 changed files with 1086 additions and 255 deletions
+278 -64
View File
@@ -1241,85 +1241,285 @@ app.delete('/api/brincar-ativo/:id', requireAuth, async (req, res) => {
});
// ============================================================
// ROTAS DO ESTÚDIO DE POESIA
// ============================================================
// ROTAS DO ESTÚDIO DE POESIAS (ESTÚDIO PROFISSIONAL)
// ============================================================
app.post('/api/poesias/generate', requireAuth, async (req, res) => {
const { faixaEtaria, estrofes, tema } = req.body;
if (!faixaEtaria || !estrofes || !tema) {
return res.status(400).json({ error: 'Faixa etária, quantidade de estrofes e tema são obrigatórios.' });
const { faixaEtaria, estrofes, tema, estrutura, estilo, voz, turmaId, gerarAudio } = req.body;
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
if (!tema) {
return res.status(400).json({ error: 'O tema da poesia é obrigatório.' });
}
try {
let poesiaText = '';
const agentConfig = readAgentConfig();
const agentName = agentConfig.agentName || "PedagogIA";
let idadeInstrucao = '';
if (faixaEtaria.includes('Bebês') || faixaEtaria.includes('bem pequenas')) {
idadeInstrucao = '- NÍVEL FÁCIL: Use palavras EXTREMAMENTE simples, do cotidiano infantil. As estrofes devem ser MUITO curtas, contendo poucas palavras por verso, pois são crianças de colo ou maternal. Elas precisam conseguir memorizar e repetir facilmente.';
} else if (faixaEtaria.includes('pequenas')) {
idadeInstrucao = '- NÍVEL MÉDIO: Use palavras fáceis e lúdicas, focando em sonoridade. As estrofes podem ter tamanho normal de versos infantis (4 a 5 palavras por verso).';
} else {
idadeInstrucao = '- NÍVEL AVANÇADO: Use vocabulário um pouco mais rico, com metáforas leves. Para crianças do Fundamental I, os versos podem ser mais elaborados e trazer reflexões sobre o tema.';
}
const promptPoesia = `Você é a ${agentName}, poetisa, linguista e assistente pedagógica da professora Camila Martella Gasparini Reifonas.
A professora Camila deseja compor uma poesia pedagógica para seus alunos na Educação Infantil.
const systemInstruction = `Você é um poeta pedagógico infantil especializado em criar poesias ritmadas, sonoras e adequadas para o desenvolvimento infantil no Brasil.
REGRAS RÍGIDAS:
- Escreva a poesia para a faixa etária: ${faixaEtaria}
${idadeInstrucao}
- A poesia DEVE ter EXATAMENTE ${estrofes} estrofes.
- O tema da poesia é: "${tema}"
- Use rimas claras, ritmo marcante (musicalidade) e seja criativo.
- Sua resposta deve conter APENAS a poesia formatada em Markdown, sem NENHUM comentário adicional, explicação, ou blocos de pensamento.
- Responda EXCLUSIVAMENTE em Português do Brasil.`;
PARÂMETROS DA POESIA:
- Tema / Ideia Central: "${tema}"
- Faixa Etária: "${faixaEtaria || 'Crianças pequenas (4 a 5 anos)'}"
- Quantidade de Estrofes: ${estrofes || 3}
- Estrutura Poética: "${estrutura || 'Quadras (Rimas A-B-A-B)'}"
- Estilo Pedagógico: "${estilo || 'Lúdico-cantado e sonoro'}"
// Utilizando o Gemini diretamente
if (process.env.GOOGLE_AI_API_KEY) {
console.log('[Poesia] Usando API direta do Google Gemini...');
const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${process.env.GOOGLE_AI_MODEL || 'gemini-2.5-flash'}:generateContent?key=${process.env.GOOGLE_AI_API_KEY}`;
const response = await fetch(geminiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: systemInstruction }] }],
generationConfig: { temperature: 0.7, maxOutputTokens: 2000 }
})
INSTRUÇÕES DE COMPOSIÇÃO:
1. Componha uma poesia envolvente, com ritmo marcante, musicalidade e rimas expressivas adequadas para a idade informada.
2. Realize uma análise literária detalhada com estimativas percentuais (0-100%) para:
- Rima (precisão das terminações sonoras)
- Ritmo (cadência e compasso dos versos)
- Aliteração (repetição harmoniosa de sons consonantais)
- Musicalidade (sonoridade geral)
3. Crie uma versão com guia fonético / marcação de leitura (com acentos tônicos em negrito e marcações de [pausa] para a leitura em voz alta).
4. Mapeie o campo de experiência da BNCC e as habilidades trabalhadas (ex: EI02EF01 / EI03EF02 - Escuta, Fala, Pensamento e Imaginação).
5. Forneça uma vivência pedagógica prática (dica para a professora Camila conduzir sarau, gestos ou dinâmicas).
Retorne ESTRITAMENTE um JSON válido (sem texto fora do JSON):
{
"titulo": "Título Criativo do Poema",
"poesia": "Texto completo da poesia com quebras de linha e separação de estrofes.",
"esquemaRima": "A-B-A-B",
"analiseLiteraria": {
"rima": 85,
"ritmo": 80,
"aliteracao": 65,
"musicalidade": 90,
"explicacaoMetrica": "Versos regulares com cadência agradável, rimas cruzadas e sonoridade envolvente para contação de histórias."
},
"versaoFonetica": "Versão da poesia com sílabas tônicas marcadas e [pausa] para orientação de leitura em sala de aula.",
"bnccCampos": "EI02EF01 / EI03EF02 • Escuta, Fala, Pensamento e Imaginação",
"dicasPedagogicas": "Sugestão de 2 a 3 linhas para a professora aplicar com as crianças em sala."
}`;
let jsonResponseText = '';
// 1. Tentar MiniMax M3
try {
console.log('[Poesia] Compondo poesia com MiniMax M3...');
const r = await callMinimax({
system: "Você é uma poetisa infantil e linguista pedagógica de excelência no Brasil. Responda estritamente em formato JSON válido.",
messages: [{ role: 'user', content: promptPoesia }],
temperature: 0.7,
max_tokens: 1800
});
jsonResponseText = r.text;
} catch (err) {
console.error('[Poesia] Erro MiniMax M3:', err.message);
}
if (!response.ok) {
const errText = await response.text();
throw new Error(`Erro na API oficial do Gemini: ${errText}`);
// 2. Fallback Groq
if (!jsonResponseText && process.env.GROQ_API_KEY) {
try {
console.log('[Poesia] Fallback Groq...');
const r = await fetch('https://api.groq.com/openai/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.GROQ_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: process.env.GROQ_MODEL || 'openai/gpt-oss-120b',
messages: [{ role: 'user', content: promptPoesia }],
temperature: 0.7,
max_tokens: 1800,
response_format: { type: 'json_object' }
})
});
if (r.ok) {
const data = await r.json();
jsonResponseText = data.choices?.[0]?.message?.content || '';
}
} catch (err) {
console.error('[Poesia] Erro Groq:', err.message);
}
const resData = await response.json();
poesiaText = resData.candidates?.[0]?.content?.parts?.[0]?.text || '';
// Remover qualquer tag <think> residual (embora a instrução já previna)
poesiaText = poesiaText.replace(/<think>[\s\S]*?(?:<\/think>|$)/g, '').trim();
} else {
throw new Error('Chave GOOGLE_AI_API_KEY não configurada no servidor.');
}
if (!poesiaText) {
throw new Error('Nenhuma poesia gerada.');
// 3. Fallback OpenRouter
if (!jsonResponseText && process.env.OPENROUTER_API_KEY) {
try {
console.log('[Poesia] Fallback OpenRouter...');
const r = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: process.env.OPENROUTER_MODEL || 'openai/gpt-4.1-nano',
messages: [{ role: 'user', content: promptPoesia }],
temperature: 0.7,
max_tokens: 1800
})
});
if (r.ok) {
const data = await r.json();
jsonResponseText = data.choices?.[0]?.message?.content || '';
}
} catch (err) {
console.error('[Poesia] Erro OpenRouter:', err.message);
}
}
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
const dbRes = await dbPool.query(
`INSERT INTO escola.poesias (usuario_id, faixa_etaria, estrofes, tema, poesia)
VALUES ($1, $2, $3, $4, $5) RETURNING id, created_at`,
[usuarioId, faixaEtaria, parseInt(estrofes), tema, poesiaText]
);
// 4. Fallback Google Gemini
if (!jsonResponseText && process.env.GOOGLE_AI_API_KEY) {
try {
console.log('[Poesia] Fallback Gemini...');
const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${process.env.GOOGLE_AI_MODEL || 'gemini-2.5-flash'}:generateContent?key=${process.env.GOOGLE_AI_API_KEY}`;
const response = await fetch(geminiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: promptPoesia }] }],
generationConfig: { temperature: 0.7, maxOutputTokens: 2000 }
})
});
if (response.ok) {
const resData = await response.json();
jsonResponseText = resData.candidates?.[0]?.content?.parts?.[0]?.text || '';
}
} catch (err) {
console.error('[Poesia] Erro Gemini:', err.message);
}
}
// Parse JSON
let poesiaData = {};
try {
const cleanJson = jsonResponseText.replace(/```json|```/g, '').trim();
poesiaData = JSON.parse(cleanJson);
} catch (parseErr) {
console.warn('[Poesia] Erro parse JSON, usando fallback texto:', parseErr.message);
poesiaData = {
titulo: `Poema: ${tema}`,
poesia: jsonResponseText || `Na escola a gente brinca,\nNa escola a gente aprende,\nCom ${tema} todo dia,\nNosso coração se acende!`,
esquemaRima: "A-B-C-B",
analiseLiteraria: {
rima: 80,
ritmo: 75,
aliteracao: 60,
musicalidade: 85,
explicacaoMetrica: "Métrica ritmada ideal para contação infantil e memorização em roda."
},
versaoFonetica: `Na es**co**la a gente **brin**ca, [pausa]\nNa es**co**la a gente a**pren**de, [pausa]\nCom ${tema} todo **di**a, [pausa]\nNosso cora**ção** se a**cen**de!`,
bnccCampos: "EI02EF01 / EI03EF02 • Escuta, Fala, Pensamento e Imaginação",
dicasPedagogicas: "Faça gestos de mãos no peito e palmas suaves ao recitar as rimas finais."
};
}
// GERAÇÃO DE ÁUDIO TTS / RECITADO (MiniMax)
let audioUrl = null;
if (gerarAudio !== false && process.env.MINIMAX_API_KEY) {
try {
console.log('[Poesia] Gerando áudio recitado com MiniMax...');
let voiceDesc = 'sweet gentle native brazilian female teacher voice reciting warm nursery poem, calm clear tempo';
if (voz === 'homem') {
voiceDesc = 'warm friendly native brazilian male educator voice storytelling poem, soft and clear';
} else if (voz === 'crianca') {
voiceDesc = 'cute enthusiastic young brazilian child voice reciting poetry, joyful';
}
const audioPrompt = `Spoken poetry recitation, ${voiceDesc}, clear pronunciation, acoustic ambient background`;
const lyricsToRecite = (poesiaData.poesia || tema).replace(/[*#]/g, '').trim();
const minmBase = (process.env.MINIMAX_API_BASE || 'https://api.minimax.io/v1').replace(/\/v1\/?$/, '');
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: lyricsToRecite.substring(0, 500),
audio_setting: { sample_rate: 32000, bitrate: 128000, format: 'mp3' }
})
});
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 = `poesia_${Date.now()}.mp3`;
const filePath = path.join(mediaDir, fileName);
const audioBuffer = Buffer.from(audioHex, 'hex');
fs.writeFileSync(filePath, audioBuffer);
await backupMediaFile(filePath, audioBuffer);
audioUrl = `/generated-media/${fileName}`;
}
}
} catch (audioErr) {
console.error('[Poesia] Erro ao gerar áudio recitado:', audioErr.message);
}
}
// Salvar no Banco PostgreSQL
let savedId = null;
try {
const dbRes = await dbPool.query(
`INSERT INTO escola.poesias (
usuario_id, faixa_etaria, estrofes, tema, poesia,
estrutura, estilo, audio_url, analise_metrica, versao_fonetica, bncc, turma_id, dicas
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id`,
[
usuarioId,
faixaEtaria || 'Crianças pequenas',
parseInt(estrofes) || 3,
tema,
poesiaData.poesia || '',
estrutura || 'Quadras',
estilo || 'Lúdico-cantado',
audioUrl,
JSON.stringify(poesiaData.analiseLiteraria || {}),
poesiaData.versaoFonetica || '',
poesiaData.bnccCampos || '',
turmaId || null,
poesiaData.dicasPedagogicas || ''
]
);
savedId = dbRes.rows[0]?.id;
} catch (dbErr) {
console.warn('[Poesia] Erro ao salvar colunas estendidas, tentando salvar básico:', dbErr.message);
try {
const dbRes2 = await dbPool.query(
`INSERT INTO escola.poesias (usuario_id, faixa_etaria, estrofes, tema, poesia) VALUES ($1, $2, $3, $4, $5) RETURNING id`,
[usuarioId, faixaEtaria || 'Crianças', parseInt(estrofes) || 3, tema, poesiaData.poesia || '']
);
savedId = dbRes2.rows[0]?.id;
} catch (err2) {
console.error('[Poesia] Erro ao salvar poesia:', err2.message);
}
}
res.json({
success: true,
id: dbRes.rows[0].id,
poesia: poesiaText,
createdAt: dbRes.rows[0].created_at
id: savedId,
titulo: poesiaData.titulo || `Poema: ${tema}`,
poesia: poesiaData.poesia || '',
esquemaRima: poesiaData.esquemaRima || 'A-B-A-B',
analiseLiteraria: poesiaData.analiseLiteraria || { rima: 80, ritmo: 80, aliteracao: 60, musicalidade: 85 },
versaoFonetica: poesiaData.versaoFonetica || '',
bnccCampos: poesiaData.bnccCampos || 'EI02EF01 / EI03EF02 • Escuta, Fala, Pensamento e Imaginação',
dicasPedagogicas: poesiaData.dicasPedagogicas || '',
audioUrl: audioUrl,
tema: tema,
faixaEtaria: faixaEtaria,
estrutura: estrutura,
estilo: estilo
});
} catch (err) {
console.error('Erro na geração de poesia:', err);
res.status(500).json({ error: 'Erro ao gerar poesia: ' + err.message });
console.error('Erro geral no Estúdio de Poesia:', err);
res.status(500).json({ error: 'Erro ao compor poesia: ' + err.message });
}
});
@@ -1327,13 +1527,20 @@ app.get('/api/poesias/list', requireAuth, async (req, res) => {
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
try {
const dbRes = await dbPool.query(
`SELECT id, faixa_etaria, estrofes, tema, created_at FROM escola.poesias WHERE usuario_id = $1 ORDER BY created_at DESC`,
`SELECT id, faixa_etaria, estrofes, tema, audio_url, created_at FROM escola.poesias WHERE usuario_id = $1 ORDER BY created_at DESC`,
[usuarioId]
);
res.json(dbRes.rows);
} catch (err) {
console.error('Erro ao listar poesias:', err);
res.status(500).json({ error: 'Erro ao listar histórico de poesias.' });
try {
const dbRes2 = await dbPool.query(
`SELECT id, faixa_etaria, estrofes, tema, created_at FROM escola.poesias WHERE usuario_id = $1 ORDER BY created_at DESC`,
[usuarioId]
);
res.json(dbRes2.rows);
} catch (err2) {
res.status(500).json({ error: 'Erro ao listar histórico de poesias.' });
}
}
});
@@ -1350,7 +1557,6 @@ app.get('/api/poesias/:id', requireAuth, async (req, res) => {
}
res.json(dbRes.rows[0]);
} catch (err) {
console.error('Erro ao buscar poesia:', err);
res.status(500).json({ error: 'Erro ao buscar poesia.' });
}
});
@@ -1364,11 +1570,10 @@ app.delete('/api/poesias/:id', requireAuth, async (req, res) => {
[id, usuarioId]
);
if (dbRes.rowCount === 0) {
return res.status(404).json({ error: 'Poesia não encontrada ou não pertence ao usuário.' });
return res.status(404).json({ error: 'Poesia não encontrada.' });
}
res.json({ success: true, message: 'Poesia deletada com sucesso.' });
} catch (err) {
console.error('Erro ao deletar poesia:', err);
res.status(500).json({ error: 'Erro ao deletar poesia.' });
}
});
@@ -2463,6 +2668,15 @@ async function initDatabase() {
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
ALTER TABLE escola.poesias ADD COLUMN IF NOT EXISTS estrutura VARCHAR(100);
ALTER TABLE escola.poesias ADD COLUMN IF NOT EXISTS estilo VARCHAR(100);
ALTER TABLE escola.poesias ADD COLUMN IF NOT EXISTS audio_url TEXT;
ALTER TABLE escola.poesias ADD COLUMN IF NOT EXISTS analise_metrica JSONB;
ALTER TABLE escola.poesias ADD COLUMN IF NOT EXISTS versao_fonetica TEXT;
ALTER TABLE escola.poesias ADD COLUMN IF NOT EXISTS bncc VARCHAR(100);
ALTER TABLE escola.poesias ADD COLUMN IF NOT EXISTS turma_id UUID;
ALTER TABLE escola.poesias ADD COLUMN IF NOT EXISTS dicas TEXT;
-- Músicas (Musicando Ideias)
CREATE TABLE IF NOT EXISTS escola.musicas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),