feat: melhoria da estabilidade de áudio, robustez de JSON, fallback Gemini e injeção real de observações no Chat

This commit is contained in:
2026-05-27 17:16:26 +00:00
parent d9be79fcfb
commit d6260747d1
2 changed files with 123 additions and 15 deletions
+13 -1
View File
@@ -2014,7 +2014,19 @@ const initApp = () => {
// Solicita permissão e inicia gravação
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus' });
let options = {};
if (typeof MediaRecorder.isTypeSupported === 'function') {
if (MediaRecorder.isTypeSupported('audio/webm;codecs=opus')) {
options = { mimeType: 'audio/webm;codecs=opus' };
} else if (MediaRecorder.isTypeSupported('audio/webm')) {
options = { mimeType: 'audio/webm' };
} else if (MediaRecorder.isTypeSupported('audio/ogg')) {
options = { mimeType: 'audio/ogg' };
} else if (MediaRecorder.isTypeSupported('audio/mp4')) {
options = { mimeType: 'audio/mp4' };
}
}
mediaRecorder = new MediaRecorder(stream, options);
mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) audioChunks.push(e.data);
+110 -14
View File
@@ -168,8 +168,8 @@ app.post('/api/conhecimento/extrair', requireAuth, (req, res) => {
const { message } = req.body;
if (!message) return res.status(400).json({ error: 'Mensagem é obrigatória' });
// Procura por padrão [KNOWLEDGE: ...] na mensagem
const knowledgeRegex = /\[KNOWLEDGE:\s*([^\]]+)\]/g;
// Procura por padrão [KNOWLEDGE: ...] na mensagem (case-insensitive)
const knowledgeRegex = /\[KNOWLEDGE:\s*([^\]]+)\]/gi;
const facts = [];
let match;
while ((match = knowledgeRegex.exec(message)) !== null) {
@@ -217,7 +217,14 @@ app.post('/api/observacao/transcrever', requireAuth, multerUpload.single('audio'
return res.status(400).json({ error: 'Nenhum áudio recebido' });
}
const tempPath = `/tmp/obs_${Date.now()}.webm`;
let ext = 'webm';
if (req.file.mimetype) {
if (req.file.mimetype.includes('mp4')) ext = 'mp4';
else if (req.file.mimetype.includes('ogg')) ext = 'ogg';
else if (req.file.mimetype.includes('wav')) ext = 'wav';
else if (req.file.mimetype.includes('mpeg')) ext = 'mp3';
}
const tempPath = `/tmp/obs_${Date.now()}.${ext}`;
try {
// Salva arquivo temporário a partir do buffer
@@ -303,18 +310,23 @@ Retorne APENAS o JSON válido:`;
// Parse JSON da resposta
let parsedData = { criancas: null, turma: 'não informada', tags: [], relatorio: rawContent };
try {
// Limpa markdown code blocks se houver
const cleanJson = rawContent.replace(/```json\s*/g, '').replace(/```\s*/g, '').trim();
const parsed = JSON.parse(cleanJson);
parsedData = {
criancas: parsed.criancas || null,
turma: parsed.turma || 'não informada',
tags: parsed.tags || [],
relatorio: parsed.relatorio || rawContent
};
const firstBrace = rawContent.indexOf('{');
const lastBrace = rawContent.lastIndexOf('}');
if (firstBrace !== -1 && lastBrace !== -1) {
const jsonString = rawContent.substring(firstBrace, lastBrace + 1);
const parsed = JSON.parse(jsonString);
parsedData = {
criancas: parsed.criancas || null,
turma: parsed.turma || 'não informada',
tags: parsed.tags || [],
relatorio: parsed.relatorio || rawContent
};
} else {
throw new Error('Nenhum JSON encontrado na resposta da IA');
}
} catch (parseErr) {
// Se não conseguir parsear JSON, retorna o texto como relatório
console.warn('Não foi possível parsear JSON do GPT, usando texto bruto');
console.warn('Não foi possível parsear JSON do GPT, usando texto bruto:', parseErr.message);
parsedData.relatorio = rawContent;
}
@@ -1393,11 +1405,72 @@ app.post('/api/chat', requireAuth, async (req, res) => {
const knowledgeText = getKnowledgeAsText();
// 1.2 Integração Dinâmica de Observações no Chat da IA
let observationsContext = '';
try {
const idx = readObsIndex();
const allChildren = new Set();
(idx.observations || []).forEach(o => {
if (o.criancas) {
o.criancas.forEach(c => allChildren.add(c.trim()));
}
});
const mentionedChildren = [];
const promptLower = promptText.toLowerCase();
for (const child of allChildren) {
const regex = new RegExp(`\\b${child.toLowerCase()}\\b`, 'i');
if (regex.test(promptLower)) {
mentionedChildren.push(child);
}
}
if (mentionedChildren.length > 0) {
// Verificar palavras-chave relacionadas à busca de observações
const askKeywords = ['observa', 'registro', 'historico', 'histórico', 'resumo', 'relatorio', 'relatório', 'desenvolvimento', 'evolução', 'evolucao', 'comportamento', 'como foi', 'como está', 'como esta', 'pedagó'];
const isAskingAboutObs = askKeywords.some(kw => promptLower.includes(kw));
if (isAskingAboutObs) {
let compiledObservations = [];
for (const child of mentionedChildren) {
const childObs = (idx.observations || []).filter(o =>
o.criancas && o.criancas.some(c => c.toLowerCase() === child.toLowerCase())
);
if (childObs.length > 0) {
// Carrega as últimas 5 observações
const lastObs = childObs.slice(0, 5);
const obsTexts = [];
for (const o of lastObs) {
const filePath = path.join(OBSERVACOES_DIR, o.date?.slice(0, 7), o.filename);
if (fs.existsSync(filePath)) {
let content = fs.readFileSync(filePath, 'utf8');
content = content.replace(/^---[\s\S]*?---\n/, '').trim();
obsTexts.push(`- Data: ${o.date} (${o.time}):\n${content}`);
}
}
if (obsTexts.length > 0) {
compiledObservations.push(`### Observações de ${child}:\n${obsTexts.join('\n\n')}`);
}
}
}
if (compiledObservations.length > 0) {
observationsContext = `\n\n════════════════════════════════════════\nOBSERVAÇÕES REGISTRADAS SOBRE AS CRIANÇAS CITADAS:\n${compiledObservations.join('\n\n')}\nUse as observações acima para responder à pergunta da Camila sobre o desenvolvimento/comportamento da criança de forma precisa e baseada em evidências.\n════════════════════════════════════════\n`;
console.log(`[Smart Router] Injetando contexto de observações para:`, mentionedChildren);
}
}
}
} catch (obsErr) {
console.error('Erro ao integrar observações no prompt da IA:', obsErr);
}
const dynamicSystemPrompt = {
role: "system",
content: KEMILY_SYSTEM_PROMPT.content
.replace(/Você é a Kemily/g, `Você é a ${agentName}`)
.replace('INFORMAÇÕES PESSOAIS:', `${knowledgeText}INFORMAÇÕES PESSOAIS:`)
.replace('INFORMAÇÕES PESSOAIS:', `${knowledgeText}${observationsContext}INFORMAÇÕES PESSOAIS:`)
};
const enrichedMessages = [dynamicSystemPrompt, ...messages];
@@ -1449,6 +1522,29 @@ app.post('/api/chat', requireAuth, async (req, res) => {
}
}
// Se o OpenRouter e Groq falharem ou retornarem erro, caímos no fallback do Google Gemini
if ((!chatResponse || !chatResponse.ok) && process.env.GOOGLE_AI_API_KEY) {
console.log('[Smart Router] Usando Google Gemini como fallback de streaming de texto...');
providerUsed = 'Google Gemini';
try {
chatResponse = await fetch('https://generativelanguage.googleapis.com/v1beta/openai/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.GOOGLE_AI_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: process.env.GOOGLE_AI_MODEL || 'gemini-2.5-flash',
messages: enrichedMessages,
stream: true,
temperature
})
});
} catch (geminiErr) {
console.error('[Smart Router] Erro crítico: Falha no fallback do Google Gemini também:', geminiErr.message);
}
}
if (!chatResponse || !chatResponse.ok) {
const errorText = chatResponse ? await chatResponse.text() : 'Sem conexão com os servidores de IA';
console.error(`Erro na API de IA (${providerUsed}):`, errorText);