Adicionado fluxo de revisão de transcrição de voz antes da análise de IA
This commit is contained in:
+115
-28
@@ -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) => {
|
||||
|
||||
+20
-2
@@ -296,8 +296,26 @@
|
||||
<!-- Estado: processando -->
|
||||
<div id="recordStateProcessing" class="record-state" style="display: none;">
|
||||
<div class="record-spinner"></div>
|
||||
<p>Analisando sua observação...</p>
|
||||
<small style="color: var(--text-muted);">Transcrevendo e gerando relatório</small>
|
||||
<p id="recordProcessingText">Analisando sua observação...</p>
|
||||
<small id="recordProcessingSubtext" style="color: var(--text-muted);">Transcrevendo e gerando relatório</small>
|
||||
</div>
|
||||
|
||||
<!-- Estado: Transcrição Concluída (Revisão) -->
|
||||
<div id="recordStateTranscribed" class="record-state" style="display: none; width: 100%;">
|
||||
<h4 style="margin-bottom: 8px; color: var(--text-main); display: flex; align-items: center; gap: 8px; width: 100%; justify-content: flex-start;">
|
||||
<span>✍️ Revisar Transcrição</span>
|
||||
</h4>
|
||||
<p class="record-hint" style="margin-bottom: 12px; text-align: left; width: 100%;">Você pode ajustar o texto abaixo antes da análise da IA:</p>
|
||||
<textarea id="transcriptionTextarea" class="transcription-textarea" style="width: 100%; min-height: 120px; padding: 12px; border-radius: 8px; border: 1px solid var(--border-light); background-color: var(--bg-card); color: var(--text-main); font-family: inherit; font-size: 14px; line-height: 1.5; resize: vertical; margin-bottom: 16px;" placeholder="Digite ou ajuste o que foi falado..."></textarea>
|
||||
<div class="preview-actions" style="display: flex; gap: 12px; width: 100%; justify-content: flex-end;">
|
||||
<button id="btnDiscardTranscription" class="btn-discard" style="flex: 1; max-width: 150px;">Descartar</button>
|
||||
<button id="btnAnalyzeTranscription" class="btn-save-report" style="flex: 1; max-width: 200px; display: flex; align-items: center; justify-content: center; gap: 8px;">
|
||||
<span>Analisar com IA</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" style="width: 16px; height: 16px;">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Estado: prévia -->
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user