Feature: Inventando Histórias profissional com 3-stage pipeline narrativa, vinculacao a turmas/alunos, ilustracoes por cena, leitor interativo, TTS narrado e exportacao PDF/TXT
This commit is contained in:
@@ -2263,7 +2263,6 @@ app.get('/api/musicas/list', requireAuth, async (req, res) => {
|
||||
);
|
||||
res.json(dbRes.rows);
|
||||
} catch (err) {
|
||||
// Fallback se audio_url não existir ainda na query
|
||||
try {
|
||||
const dbRes2 = await dbPool.query(`SELECT id, faixa_etaria, ritmo, tema, created_at FROM escola.musicas WHERE usuario_id = $1 ORDER BY created_at DESC`, [usuarioId]);
|
||||
res.json(dbRes2.rows);
|
||||
@@ -2292,71 +2291,419 @@ app.delete('/api/musicas/:id', requireAuth, async (req, res) => {
|
||||
|
||||
|
||||
// ============================================================
|
||||
// ROTAS DO INVENTANDO HISTÓRIAS
|
||||
// ROTAS DO INVENTANDO HISTÓRIAS (ESTÚDIO PROFISSIONAL)
|
||||
// ============================================================
|
||||
app.post('/api/historias/generate', requireAuth, async (req, res) => {
|
||||
const { faixaEtaria, paragrafos, personagens, estilo, tema } = req.body;
|
||||
const {
|
||||
faixaEtaria,
|
||||
paragrafos,
|
||||
personagens,
|
||||
estilo,
|
||||
tema,
|
||||
personagemPrincipal,
|
||||
objetoMagico,
|
||||
estiloVisual,
|
||||
turmaId,
|
||||
gerarAudio
|
||||
} = req.body;
|
||||
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
|
||||
if (!faixaEtaria || !paragrafos || !personagens || !estilo || !tema) {
|
||||
return res.status(400).json({ error: 'Todos os campos são obrigatórios.' });
|
||||
|
||||
if (!tema) {
|
||||
return res.status(400).json({ error: 'O tema da história é obrigatório.' });
|
||||
}
|
||||
|
||||
try {
|
||||
let idadeInstrucao = '';
|
||||
if (faixaEtaria.includes('Bebês') || faixaEtaria.includes('bem pequenas')) {
|
||||
idadeInstrucao = '- NÍVEL FÁCIL: Enredo muito simples, frases curtas, foco em ações diretas.';
|
||||
} else {
|
||||
idadeInstrucao = '- NÍVEL MÉDIO/AVANÇADO: Enredo com lição de moral, imaginação fluida e bom vocabulário.';
|
||||
const agentConfig = readAgentConfig();
|
||||
const agentName = agentConfig.agentName || "PedagogIA";
|
||||
|
||||
const promptHistoria = `Você é a ${agentName}, contadora de histórias e escritora de literatura infantil da professora Camila Martella Gasparini Reifonas.
|
||||
A professora Camila deseja inventar uma história pedagógica envolvente para seus alunos na Educação Infantil.
|
||||
|
||||
PARÂMETROS DA HISTÓRIA:
|
||||
- Tema / Enredo: "${tema}"
|
||||
- Faixa Etária do Público: "${faixaEtaria || 'Crianças pequenas (4 a 5 anos)'}"
|
||||
- Personagem Principal: "${personagemPrincipal || 'Criança curiosa da turma'}"
|
||||
- Objeto / Elemento Mágico: "${objetoMagico || 'Arca da Imaginação'}"
|
||||
- Estilo Narrativo: "${estilo || 'Lúdico, Afetivo e Dialogado'}"
|
||||
- Estilo Visual das Ilustrações: "${estiloVisual || 'Cartoon colorido e acolhedor'}"
|
||||
- Quantidade de Cenas / Atos: ${paragrafos || 3}
|
||||
|
||||
ESTRUTURA PEDAGÓGICA (3 ATOS):
|
||||
1. Ato 1 (Introdução): Apresentação do personagem no cotidiano escolar/infantil e descoberta do elemento disparador.
|
||||
2. Ato 2 (Conflito / Desafio): Uma situação-problema lúdica onde a cooperação, empatia e inteligência emocional são necessárias.
|
||||
3. Ato 3 (Resolução & Aprendizado): Resolução mágica com celebração da amizade e aprendizado afetivo.
|
||||
|
||||
Retorne ESTRITAMENTE um JSON válido (sem tags markdown externas):
|
||||
{
|
||||
"titulo": "Título Mágico e Divertido da História",
|
||||
"personagemPrincipal": "${personagemPrincipal || 'Personagem Principal'}",
|
||||
"objetoMagico": "${objetoMagico || 'Elemento Especial'}",
|
||||
"historiaCompleta": "Texto narrativo completo e fluido da história com quebras de parágrafos.",
|
||||
"cenas": [
|
||||
{
|
||||
"numero": 1,
|
||||
"titulo": "Ato 1: O Descobrimento Mágico",
|
||||
"texto": "Texto do primeiro ato para contação...",
|
||||
"promptImagem": "Cute children book illustration of ${personagemPrincipal || 'a happy child'} finding a magical glowing box in a vibrant kindergarten garden, ${estiloVisual || 'cartoon style'}, bright warm colors, highly detailed"
|
||||
},
|
||||
{
|
||||
"numero": 2,
|
||||
"titulo": "Ato 2: A Grande Descoberta e o Desafio",
|
||||
"texto": "Texto do segundo ato com a situação-problema...",
|
||||
"promptImagem": "Children book illustration of friends cooperating together in a classroom, ${estiloVisual || 'cartoon style'}, friendly smiles, soft lighting"
|
||||
},
|
||||
{
|
||||
"numero": 3,
|
||||
"titulo": "Ato 3: A Festa da Amizade",
|
||||
"texto": "Texto do terceiro ato com o desfecho feliz...",
|
||||
"promptImagem": "Children celebration in a colorful school yard with balloons, ${estiloVisual || 'cartoon style'}, joyful atmosphere, happy children"
|
||||
}
|
||||
],
|
||||
"perguntasCompreensao": [
|
||||
"1. O que o personagem descobriu no início da história?",
|
||||
"2. Como os amigos ajudaram a resolver o desafio?",
|
||||
"3. O que você faria se encontrasse esse objeto mágico?"
|
||||
],
|
||||
"bnccCampos": "EI02EF04 / EI03EF05 • Escuta, Fala, Pensamento e Imaginação",
|
||||
"duracaoLeitura": "3 a 4 minutos",
|
||||
"complexidade": "Adequada para Educação Infantil (Lúdica e Sonora)"
|
||||
}`;
|
||||
|
||||
const systemInstruction = `Você é um contador de histórias pedagógico focado na educação infantil brasileira.
|
||||
REGRAS:
|
||||
- Faixa etária: ${faixaEtaria}
|
||||
${idadeInstrucao}
|
||||
- Tamanho: Exatamente ${paragrafos} parágrafos.
|
||||
- Quantidade de personagens: ${personagens}
|
||||
- Estilo narrativo: ${estilo} (se Dialogada, inclua falas claras entre os personagens; se Narração Direta, apenas o narrador conta tudo).
|
||||
- Tema: "${tema}"
|
||||
- Responda APENAS a história em Markdown.`;
|
||||
let jsonResponseText = '';
|
||||
|
||||
let historiaText = '';
|
||||
if (process.env.GOOGLE_AI_API_KEY) {
|
||||
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 }] }] })
|
||||
// 1. Tentar MiniMax M3
|
||||
try {
|
||||
console.log('[Histórias] Criando narrativa com MiniMax M3...');
|
||||
const r = await callMinimax({
|
||||
system: "Você é uma autora premiada de literatura infantil e contadora de histórias pedagógica no Brasil. Responda estritamente em JSON válido.",
|
||||
messages: [{ role: 'user', content: promptHistoria }],
|
||||
temperature: 0.7,
|
||||
max_tokens: 2500
|
||||
});
|
||||
if (!response.ok) {
|
||||
const txt = await response.text();
|
||||
throw new Error(`Gemini Error: ${txt}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
historiaText = data.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
||||
jsonResponseText = r.text;
|
||||
} catch (err) {
|
||||
console.error('[Histórias] Erro MiniMax M3:', err.message);
|
||||
}
|
||||
|
||||
historiaText = historiaText.replace(/<think>[\s\S]*?<\/think>/gi, '').trim();
|
||||
|
||||
if (!historiaText) throw new Error('Falha ao inventar história (texto vazio)');
|
||||
// 2. Fallback Groq
|
||||
if (!jsonResponseText && process.env.GROQ_API_KEY) {
|
||||
try {
|
||||
console.log('[Histórias] 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: promptHistoria }],
|
||||
temperature: 0.7,
|
||||
max_tokens: 2500,
|
||||
response_format: { type: 'json_object' }
|
||||
})
|
||||
});
|
||||
if (r.ok) {
|
||||
const data = await r.json();
|
||||
jsonResponseText = data.choices?.[0]?.message?.content || '';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Histórias] Erro Groq:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
const dbRes = await dbPool.query(
|
||||
`INSERT INTO escola.historias (usuario_id, faixa_etaria, paragrafos, personagens, estilo, tema, historia) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`,
|
||||
[usuarioId, faixaEtaria, paragrafos, personagens, estilo, tema, historiaText]
|
||||
);
|
||||
// 3. Fallback OpenRouter
|
||||
if (!jsonResponseText && process.env.OPENROUTER_API_KEY) {
|
||||
try {
|
||||
console.log('[Histórias] 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: promptHistoria }],
|
||||
temperature: 0.7,
|
||||
max_tokens: 2500
|
||||
})
|
||||
});
|
||||
if (r.ok) {
|
||||
const data = await r.json();
|
||||
jsonResponseText = data.choices?.[0]?.message?.content || '';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Histórias] Erro OpenRouter:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Fallback Google Gemini
|
||||
if (!jsonResponseText && process.env.GOOGLE_AI_API_KEY) {
|
||||
try {
|
||||
console.log('[Histórias] 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: promptHistoria }] }],
|
||||
generationConfig: { temperature: 0.7, maxOutputTokens: 2500 }
|
||||
})
|
||||
});
|
||||
if (response.ok) {
|
||||
const resData = await response.json();
|
||||
jsonResponseText = resData.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Histórias] Erro Gemini:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse JSON
|
||||
let historiaData = {};
|
||||
try {
|
||||
const cleanJson = jsonResponseText.replace(/```json|```/g, '').trim();
|
||||
historiaData = JSON.parse(cleanJson);
|
||||
} catch (parseErr) {
|
||||
console.warn('[Histórias] Erro parse JSON, criando estrutura de contingência:', parseErr.message);
|
||||
historiaData = {
|
||||
titulo: `A Aventura de ${personagemPrincipal || 'Nossa Turma'}`,
|
||||
personagemPrincipal: personagemPrincipal || 'Aluno(a)',
|
||||
objetoMagico: objetoMagico || 'Elemento Especial',
|
||||
historiaCompleta: jsonResponseText || `Era uma vez, em um dia cheio de sol na escola, ${personagemPrincipal || 'uma criança'} encontrou um objeto mágico...`,
|
||||
cenas: [
|
||||
{ numero: 1, titulo: "Ato 1: O Início da Jornada", texto: "A turma estava reunida quando algo incrível aconteceu...", promptImagem: "Children in kindergarten playing joyfully" },
|
||||
{ numero: 2, titulo: "Ato 2: O Desafio", texto: "Juntos, eles precisavam cooperar para desvendar o segredo...", promptImagem: "Kids cooperating and sharing" },
|
||||
{ numero: 3, titulo: "Ato 3: O Final Feliz", texto: "Com muitos abraços e alegria, a história teve o final perfeito!", promptImagem: "Happy children celebrating" }
|
||||
],
|
||||
perguntasCompreensao: ["O que aconteceu no início?", "Como eles resolveram o problema?"],
|
||||
bnccCampos: "EI02EF04 / EI03EF05 • Escuta, Fala, Pensamento e Imaginação",
|
||||
duracaoLeitura: "3 minutos",
|
||||
complexidade: "Lúdica (Infantil)"
|
||||
};
|
||||
}
|
||||
|
||||
// Geração de Áudio Narrado (MiniMax)
|
||||
let audioUrl = null;
|
||||
if (gerarAudio !== false && process.env.MINIMAX_API_KEY) {
|
||||
try {
|
||||
console.log('[Histórias] Gerando áudio narrado com MiniMax...');
|
||||
const textToNarrate = (historiaData.historiaCompleta || tema).substring(0, 600);
|
||||
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: 'Gentle warm kindergarten teacher storytelling narration, Portuguese Brazilian accent, calm acoustic ambient',
|
||||
lyrics: textToNarrate,
|
||||
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 = `historia_${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('[Histórias] Erro ao gerar áudio narrado:', audioErr.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Salvar no Banco PostgreSQL
|
||||
let savedId = null;
|
||||
try {
|
||||
const dbRes = await dbPool.query(
|
||||
`INSERT INTO escola.historias (
|
||||
usuario_id, faixa_etaria, paragrafos, personagens, estilo, tema, historia,
|
||||
titulo, personagem_principal, objeto_magico, cenas, bncc, turma_id, audio_url,
|
||||
perguntas_compreensao, duracao_leitura, complexidade, estilo_visual
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) RETURNING id`,
|
||||
[
|
||||
usuarioId,
|
||||
faixaEtaria || 'Crianças pequenas',
|
||||
parseInt(paragrafos) || 3,
|
||||
parseInt(personagens) || 2,
|
||||
estilo || 'Dialogada',
|
||||
tema,
|
||||
historiaData.historiaCompleta || '',
|
||||
historiaData.titulo || `História: ${tema}`,
|
||||
personagemPrincipal || '',
|
||||
objetoMagico || '',
|
||||
JSON.stringify(historiaData.cenas || []),
|
||||
historiaData.bnccCampos || '',
|
||||
turmaId || null,
|
||||
audioUrl,
|
||||
JSON.stringify(historiaData.perguntasCompreensao || []),
|
||||
historiaData.duracaoLeitura || '3 min',
|
||||
historiaData.complexidade || 'Lúdica',
|
||||
estiloVisual || 'Cartoon colorido'
|
||||
]
|
||||
);
|
||||
savedId = dbRes.rows[0]?.id;
|
||||
} catch (dbErr) {
|
||||
console.warn('[Histórias] Erro ao salvar colunas estendidas, salvando básico:', dbErr.message);
|
||||
try {
|
||||
const dbRes2 = await dbPool.query(
|
||||
`INSERT INTO escola.historias (usuario_id, faixa_etaria, paragrafos, personagens, estilo, tema, historia) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`,
|
||||
[usuarioId, faixaEtaria || 'Crianças', parseInt(paragrafos) || 3, parseInt(personagens) || 2, estilo || 'Lúdico', tema, historiaData.historiaCompleta || '']
|
||||
);
|
||||
savedId = dbRes2.rows[0]?.id;
|
||||
} catch (err2) {
|
||||
console.error('[Histórias] Erro ao salvar história:', err2.message);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
id: savedId,
|
||||
titulo: historiaData.titulo || `História: ${tema}`,
|
||||
personagemPrincipal: historiaData.personagemPrincipal || personagemPrincipal,
|
||||
objetoMagico: historiaData.objetoMagico || objetoMagico,
|
||||
historiaCompleta: historiaData.historiaCompleta || '',
|
||||
cenas: historiaData.cenas || [],
|
||||
perguntasCompreensao: historiaData.perguntasCompreensao || [],
|
||||
bnccCampos: historiaData.bnccCampos || 'EI02EF04 / EI03EF05 • Escuta, Fala, Pensamento e Imaginação',
|
||||
duracaoLeitura: historiaData.duracaoLeitura || '3 a 4 min',
|
||||
complexidade: historiaData.complexidade || 'Lúdica e Sonora',
|
||||
audioUrl: audioUrl,
|
||||
tema: tema,
|
||||
faixaEtaria: faixaEtaria,
|
||||
estilo: estilo,
|
||||
estiloVisual: estiloVisual
|
||||
});
|
||||
|
||||
res.json({ id: dbRes.rows[0].id, historia: historiaText });
|
||||
} catch (err) {
|
||||
console.error('Erro História:', err);
|
||||
res.status(500).json({ error: 'Erro ao inventar história: ' + err.message });
|
||||
console.error('Erro ao inventar história:', err);
|
||||
res.status(500).json({ error: 'Erro ao gerar história: ' + err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Endpoint para gerar ilustração individual de uma cena
|
||||
app.post('/api/historias/scene-image', requireAuth, async (req, res) => {
|
||||
const { prompt, estiloVisual, cenaNumero } = req.body;
|
||||
if (!prompt) {
|
||||
return res.status(400).json({ error: 'O prompt da cena é obrigatório.' });
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`[Histórias] Gerando ilustração para cena ${cenaNumero || 1}...`);
|
||||
const enhancedPrompt = `${prompt}, ${estiloVisual || 'children book colorful illustration'}, vibrant lighting, cute warm character design, 4k master artwork`;
|
||||
|
||||
// 1. Tentar MiniMax image-01
|
||||
if (process.env.MINIMAX_API_KEY) {
|
||||
try {
|
||||
const minmBase = (process.env.MINIMAX_API_BASE || 'https://api.minimax.io/v1').replace(/\/v1\/?$/, '');
|
||||
const imgResp = await fetch(`${minmBase}/v1/image_generation`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.MINIMAX_API_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'image-01',
|
||||
prompt: enhancedPrompt,
|
||||
aspect_ratio: '4:3',
|
||||
response_format: 'base64'
|
||||
})
|
||||
});
|
||||
|
||||
if (imgResp.ok) {
|
||||
const imgData = await imgResp.json();
|
||||
const base64Data = imgData.data?.image_base64 || imgData.data?.images?.[0];
|
||||
if (base64Data) {
|
||||
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 = `cena_${Date.now()}.png`;
|
||||
const filePath = path.join(mediaDir, fileName);
|
||||
const buffer = Buffer.from(base64Data, 'base64');
|
||||
fs.writeFileSync(filePath, buffer);
|
||||
await backupMediaFile(filePath, buffer);
|
||||
return res.json({ success: true, imageUrl: `/generated-media/${fileName}` });
|
||||
}
|
||||
}
|
||||
} catch (minErr) {
|
||||
console.warn('[Histórias] Falha MiniMax imagem:', minErr.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback OpenRouter FLUX/SD
|
||||
if (process.env.OPENROUTER_API_KEY) {
|
||||
try {
|
||||
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: 'black-forest-labs/flux-1-schnell',
|
||||
messages: [{ role: 'user', content: enhancedPrompt }]
|
||||
})
|
||||
});
|
||||
if (r.ok) {
|
||||
const data = await r.json();
|
||||
const imgUrl = data.choices?.[0]?.message?.content;
|
||||
if (imgUrl && imgUrl.startsWith('http')) {
|
||||
return res.json({ success: true, imageUrl: imgUrl });
|
||||
}
|
||||
}
|
||||
} catch (orErr) {
|
||||
console.warn('[Histórias] Falha OpenRouter imagem:', orErr.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback Imagem Ilustrada Placeholder SVG
|
||||
res.json({
|
||||
success: true,
|
||||
imageUrl: 'https://images.unsplash.com/photo-1512820790803-83ca734da794?w=600&auto=format&fit=crop&q=80'
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('[Histórias] Erro ao gerar imagem da cena:', err);
|
||||
res.status(500).json({ error: 'Erro ao gerar imagem da cena: ' + err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/historias/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, estilo, tema, created_at FROM escola.historias WHERE usuario_id = $1 ORDER BY created_at DESC`, [usuarioId]);
|
||||
const dbRes = await dbPool.query(
|
||||
`SELECT id, titulo, faixa_etaria, estilo, tema, personagem_principal, audio_url, created_at FROM escola.historias 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.' }); }
|
||||
} catch (err) {
|
||||
try {
|
||||
const dbRes2 = await dbPool.query(
|
||||
`SELECT id, faixa_etaria, estilo, tema, created_at FROM escola.historias 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 histórias.' });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/historias/:id', requireAuth, async (req, res) => {
|
||||
@@ -2766,6 +3113,18 @@ async function initDatabase() {
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
ALTER TABLE escola.historias ADD COLUMN IF NOT EXISTS titulo VARCHAR(255);
|
||||
ALTER TABLE escola.historias ADD COLUMN IF NOT EXISTS personagem_principal VARCHAR(255);
|
||||
ALTER TABLE escola.historias ADD COLUMN IF NOT EXISTS objeto_magico VARCHAR(255);
|
||||
ALTER TABLE escola.historias ADD COLUMN IF NOT EXISTS cenas JSONB;
|
||||
ALTER TABLE escola.historias ADD COLUMN IF NOT EXISTS bncc VARCHAR(100);
|
||||
ALTER TABLE escola.historias ADD COLUMN IF NOT EXISTS turma_id UUID;
|
||||
ALTER TABLE escola.historias ADD COLUMN IF NOT EXISTS audio_url TEXT;
|
||||
ALTER TABLE escola.historias ADD COLUMN IF NOT EXISTS perguntas_compreensao JSONB;
|
||||
ALTER TABLE escola.historias ADD COLUMN IF NOT EXISTS duracao_leitura VARCHAR(50);
|
||||
ALTER TABLE escola.historias ADD COLUMN IF NOT EXISTS complexidade VARCHAR(50);
|
||||
ALTER TABLE escola.historias ADD COLUMN IF NOT EXISTS estilo_visual VARCHAR(100);
|
||||
|
||||
-- Cartazes (Criar Cartazes)
|
||||
CREATE TABLE IF NOT EXISTS escola.cartazes (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
Reference in New Issue
Block a user