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