diff --git a/public/app.js b/public/app.js index 29975e1..2afb453 100644 --- a/public/app.js +++ b/public/app.js @@ -1993,6 +1993,14 @@ const initApp = () => { const btnDiscardReport = document.getElementById('btnDiscardReport'); const btnSaveReport = document.getElementById('btnSaveReport'); + // Novos elementos de transcrição + const recordStateTranscribed = document.getElementById('recordStateTranscribed'); + const transcriptionTextarea = document.getElementById('transcriptionTextarea'); + const btnDiscardTranscription = document.getElementById('btnDiscardTranscription'); + const btnAnalyzeTranscription = document.getElementById('btnAnalyzeTranscription'); + const recordProcessingText = document.getElementById('recordProcessingText'); + const recordProcessingSubtext = document.getElementById('recordProcessingSubtext'); + let mediaRecorder = null; let audioChunks = []; let recordingStartTime = null; @@ -2002,6 +2010,40 @@ const initApp = () => { let generatedTurma = 'não informada'; let generatedTags = []; + // Fechar modal - colocada no topo para evitar problemas de inicialização + function closeRecordModal() { + if (mediaRecorder && mediaRecorder.state !== 'inactive') { + try { + mediaRecorder.stop(); + } catch (e) {} + if (mediaRecorder.stream) { + mediaRecorder.stream.getTracks().forEach(t => t.stop()); + } + } + clearInterval(timerInterval); + audioChunks = []; + generatedReport = null; + recordModal.style.display = 'none'; + } + + // Função para controlar a exibição dos estados + function showRecordState(state) { + recordStateRecording.style.display = 'none'; + recordStateProcessing.style.display = 'none'; + if (recordStateTranscribed) recordStateTranscribed.style.display = 'none'; + recordStatePreview.style.display = 'none'; + + if (state === 'recording') { + recordStateRecording.style.display = 'flex'; + } else if (state === 'processing') { + recordStateProcessing.style.display = 'flex'; + } else if (state === 'transcribed') { + if (recordStateTranscribed) recordStateTranscribed.style.display = 'flex'; + } else if (state === 'preview') { + recordStatePreview.style.display = 'flex'; + } + } + // Abrir modal e iniciar gravação if (fabRecord) { fabRecord.addEventListener('click', async () => { @@ -2009,9 +2051,7 @@ const initApp = () => { generatedReport = null; // Mostra estado inicial (gravando) - recordStateRecording.style.display = 'flex'; - recordStateProcessing.style.display = 'none'; - recordStatePreview.style.display = 'none'; + showRecordState('recording'); recordModal.style.display = 'flex'; // Inicia timer @@ -2056,20 +2096,25 @@ const initApp = () => { const mins = Math.floor(elapsed / 60).toString().padStart(2, '0'); const secs = (elapsed % 60).toString().padStart(2, '0'); recordTimer.textContent = `${mins}:${secs}`; - }; + } - // Parar gravação e processar + // Parar gravação e transcrever if (btnStopRecord) { btnStopRecord.addEventListener('click', async () => { if (mediaRecorder && mediaRecorder.state !== 'inactive') { - mediaRecorder.stop(); - mediaRecorder.stream.getTracks().forEach(t => t.stop()); + try { + mediaRecorder.stop(); + } catch (e) {} + if (mediaRecorder.stream) { + mediaRecorder.stream.getTracks().forEach(t => t.stop()); + } } clearInterval(timerInterval); - // Mostra processando - recordStateRecording.style.display = 'none'; - recordStateProcessing.style.display = 'flex'; + // Mostra processando com texto de transcrição + if (recordProcessingText) recordProcessingText.textContent = 'Transcrevendo áudio...'; + if (recordProcessingSubtext) recordProcessingSubtext.textContent = 'Aguarde a transcrição por Whisper'; + showRecordState('processing'); // Envia áudio para servidor const blob = new Blob(audioChunks, { type: 'audio/webm' }); @@ -2081,6 +2126,54 @@ const initApp = () => { method: 'POST', body: formData }); + + if (!res.ok) { + const errData = await res.json(); + throw new Error(errData.error || 'Erro na transcrição'); + } + + const data = await res.json(); + + // Exibe o texto transcrito na área de texto para edição + if (transcriptionTextarea) { + transcriptionTextarea.value = data.transcribedText || ''; + } + + showRecordState('transcribed'); + } catch (err) { + console.error('Erro ao processar observação:', err); + alert('Erro ao transcrever áudio: ' + err.message); + closeRecordModal(); + } + }); + } + + // Ação de analisar transcrição + if (btnAnalyzeTranscription) { + btnAnalyzeTranscription.addEventListener('click', async () => { + const text = transcriptionTextarea ? transcriptionTextarea.value.trim() : ''; + if (!text || text.length < 5) { + alert('Por favor, digite ou revise a transcrição (mínimo de 5 caracteres).'); + return; + } + + // Mostra processando com texto de análise + if (recordProcessingText) recordProcessingText.textContent = 'Analisando observação...'; + if (recordProcessingSubtext) recordProcessingSubtext.textContent = 'Gerando relatório pedagógico estruturado'; + showRecordState('processing'); + + try { + const res = await fetch('/api/observacao/analisar', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ transcribedText: text }) + }); + + if (!res.ok) { + const errData = await res.json(); + throw new Error(errData.error || 'Erro na análise'); + } + const data = await res.json(); generatedReport = data.relatorio || data.report; generatedCriancas = data.criancas; @@ -2088,17 +2181,23 @@ const initApp = () => { generatedTags = data.tags || []; showReportPreview(data); } catch (err) { - console.error('Erro ao processar observação:', err); - alert('Erro ao processar sua observação. Tente novamente.'); - closeRecordModal(); + console.error('Erro ao analisar observação:', err); + alert('Erro ao analisar observação: ' + err.message); + showRecordState('transcribed'); } }); } + // Ação de descartar transcrição + if (btnDiscardTranscription) { + btnDiscardTranscription.addEventListener('click', () => { + closeRecordModal(); + }); + } + // Mostrar prévia do relatório function showReportPreview(data) { - recordStateProcessing.style.display = 'none'; - recordStatePreview.style.display = 'flex'; + showRecordState('preview'); const now = new Date(); previewDateTime.textContent = now.toLocaleString('pt-BR', { @@ -2123,7 +2222,7 @@ const initApp = () => { ${marked.parse(generatedReport || 'Relatório não disponível.')} `; reportContent.querySelectorAll('pre code').forEach(b => hljs.highlightElement(b)); - }; + } // Descartar relatório if (btnDiscardReport) { @@ -2173,18 +2272,6 @@ const initApp = () => { btnExitRecord.addEventListener('click', closeRecordModal); } - // Fechar modal - function closeRecordModal() { - if (mediaRecorder && mediaRecorder.state !== 'inactive') { - mediaRecorder.stop(); - mediaRecorder.stream.getTracks().forEach(t => t.stop()); - } - clearInterval(timerInterval); - audioChunks = []; - generatedReport = null; - recordModal.style.display = 'none'; - }; - // Fechar clicando fora if (recordModal) { recordModal.addEventListener('click', (e) => { diff --git a/public/index.html b/public/index.html index e093e6d..dd571fd 100644 --- a/public/index.html +++ b/public/index.html @@ -296,8 +296,26 @@ + + + diff --git a/server.js b/server.js index 20c83a8..c54d115 100644 --- a/server.js +++ b/server.js @@ -342,7 +342,7 @@ async function initDatabase() { } } -// Rota: POST /api/observacao/transcrever — recebe áudio, transcreve via Whisper, gera relatório +// Rota: POST /api/observacao/transcrever — recebe áudio e transcreve via Whisper app.post('/api/observacao/transcrever', requireAuth, multerUpload.single('audio'), async (req, res) => { if (!req.file) { return res.status(400).json({ error: 'Nenhum áudio recebido' }); @@ -355,18 +355,15 @@ app.post('/api/observacao/transcrever', requireAuth, multerUpload.single('audio' 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 - fs.writeFileSync(tempPath, req.file.buffer); - let transcribedText = ''; + const fileBlob = new Blob([req.file.buffer], { type: req.file.mimetype || 'audio/webm' }); if (process.env.OPENAI_API_KEY) { console.log('[Smart Transcription] Usando OpenAI Whisper...'); const whisperFormData = new FormData(); - whisperFormData.append('file', fs.createReadStream(tempPath)); + whisperFormData.append('file', fileBlob, `audio.${ext}`); whisperFormData.append('model', 'whisper-1'); whisperFormData.append('language', 'pt'); whisperFormData.append('response_format', 'text'); @@ -386,9 +383,9 @@ app.post('/api/observacao/transcrever', requireAuth, multerUpload.single('audio' } transcribedText = await whisperRes.text(); } else if (process.env.GROQ_API_KEY) { - console.log('[Smart Transcription] Usando Groq Whisper como fallback...'); + console.log('[Smart Transcription] Usando Groq Whisper...'); const whisperFormData = new FormData(); - whisperFormData.append('file', fs.createReadStream(tempPath)); + whisperFormData.append('file', fileBlob, `audio.${ext}`); whisperFormData.append('model', 'whisper-large-v3'); whisperFormData.append('language', 'pt'); whisperFormData.append('response_format', 'text'); @@ -411,34 +408,49 @@ app.post('/api/observacao/transcrever', requireAuth, multerUpload.single('audio' throw new Error('Nenhuma chave de API configurada para transcrição de áudio (OPENAI_API_KEY ou GROQ_API_KEY).'); } - fs.unlinkSync(tempPath); // Limpa tmp - - if (!transcribedText || transcribedText.trim().length < 5) { - return res.json({ report: '⚠️ O áudio estava muito curto ou não pôde ser transcrito. Tente gravar por mais alguns segundos.' }); + if (!transcribedText || transcribedText.trim().length < 2) { + return res.status(400).json({ error: 'O áudio não contém fala identificável.' }); } - // Gera relatório estruturado via GPT - // Envia transcrição + pedido de extrair: crianças, turma, tags, relatório - const reportPrompt = `Você é a Kemily, assistente pedagógica da professora Camila Martella Gasparini Reifonas. + res.json({ transcribedText }); -Ela acabou de gravar uma observação em voz sobre seus alunos da educação infantil. Sua tarefa é ANALISAR a transcrição e retornar UM OBJETO JSON (sem markdown, sem código, apenas o JSON puro) com: + } catch (err) { + console.error('Erro transcrever:', err); + res.status(500).json({ error: err.message }); + } +}); + +// Rota: POST /api/observacao/analisar — recebe texto transcrito e gera relatório pedagógico +app.post('/api/observacao/analisar', requireAuth, async (req, res) => { + const { transcribedText } = req.body; + if (!transcribedText || transcribedText.trim().length < 5) { + return res.status(400).json({ error: 'Texto muito curto ou inexistente para análise.' }); + } + + try { + const agentConfig = readAgentConfig(); + const agentName = agentConfig.agentName || "Kemily"; + + const reportPrompt = `Você é a ${agentName}, assistente pedagógica da professora Camila Martella Gasparini Reifonas. + +Ela acabou de registrar uma observação sobre seus alunos da educação infantil. Sua tarefa é ANALISAR o texto e retornar UM OBJETO JSON (sem markdown, sem código, apenas o JSON puro) com: { "criancas": ["lista de nomes de crianças mencionadas ou 'null' se não identificar nenhuma"], "turma": "turma mencionada ou 'não informada'", - "tags": ["tag1", "tag2"] (social, cognitivo, motor, linguagem, emocional,轮的哪个), + "tags": ["tag1", "tag2"] (social, cognitivo, motor, linguagem, emocional, colaborativo, autónomo), "relatorio": "...markdown estruturado do relatório pedagógico..." } REGRAS: -- Identifique crianças pelo nome próprio mencionado na fala +- Identifique crianças pelo nome próprio mencionado na fala/texto - Se não conseguir identificar nomes, use "criancas": null - Tags: escolha entre social, cognitivo, motor, linguagem, emocional, colaborativo, autónomo - Relatório em português, linguagem formal-pedagógica, seções claras em **markdown** - Foque em evidências observáveis - Máximo 120 palavras no relatório -TRANSCRIÇÃO: +OBSERVAÇÃO REGISTRADA: """ ${transcribedText} """ @@ -583,19 +595,14 @@ Retorne APENAS o JSON válido:`; 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 da resposta da IA:', parseErr.message); parsedData.relatorio = rawContent; } - res.json({ - transcribedText, - ...parsedData - }); + res.json(parsedData); } catch (err) { - console.error('Erro transcrever/salvar:', err); - if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); + console.error('Erro ao analisar observação:', err); res.status(500).json({ error: err.message }); } });