diff --git a/CHANGELOG.md b/CHANGELOG.md index 7af05cf..aa19bd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,13 @@ # Changelog -## [1.1.0] - 2024-01-27 - -### Adicionado -- Função de processamento de áudio (process-audio) -- Integração com OpenAI Whisper para transcrição -- Análise de leitura usando GPT-4 -- Database Trigger para processamento automático de gravações - -### Técnico -- Implementação de Edge Function no Supabase -- Configuração de ambiente de desenvolvimento com Docker -- Integração com banco de dados para story_recordings -- Sistema de análise de métricas de leitura +## [1.1.0] - 2024-03-21 ### Modificado -- Estrutura de armazenamento de gravações -- Fluxo de processamento de áudio automatizado \ No newline at end of file +- Melhorado o processo de upload de áudio para evitar colisões de arquivos e garantir integridade dos dados +- Implementado processamento assíncrono de áudio via Edge Function + +### Técnico +- Adicionado UUID para identificação única de arquivos de áudio +- Implementada transação atômica para upload de áudio +- Integrada chamada assíncrona para processamento de áudio +- Melhorado tratamento de erros no processo de upload \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 144ae24..9a40a31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,12 +24,14 @@ "react-router-dom": "^6.28.0", "recharts": "^2.15.0", "resend": "^3.2.0", - "tailwind-merge": "^2.5.5" + "tailwind-merge": "^2.5.5", + "uuid": "^11.0.3" }, "devDependencies": { "@eslint/js": "^9.9.1", "@types/react": "^18.3.17", "@types/react-dom": "^18.3.5", + "@types/uuid": "^10.0.0", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.20", "eslint": "^9.9.1", @@ -2473,6 +2475,13 @@ "source-map": "^0.6.1" } }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/webpack": { "version": "4.41.40", "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.40.tgz", @@ -6356,6 +6365,19 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/uuid": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", + "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/victory-vendor": { "version": "36.9.2", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", diff --git a/package.json b/package.json index 5bdccc5..8b9d969 100644 --- a/package.json +++ b/package.json @@ -31,12 +31,14 @@ "react-router-dom": "^6.28.0", "recharts": "^2.15.0", "resend": "^3.2.0", - "tailwind-merge": "^2.5.5" + "tailwind-merge": "^2.5.5", + "uuid": "^11.0.3" }, "devDependencies": { "@eslint/js": "^9.9.1", "@types/react": "^18.3.17", "@types/react-dom": "^18.3.5", + "@types/uuid": "^10.0.0", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.20", "eslint": "^9.9.1", diff --git a/src/components/story/AudioRecorder.tsx b/src/components/story/AudioRecorder.tsx index be7f189..88ec752 100644 --- a/src/components/story/AudioRecorder.tsx +++ b/src/components/story/AudioRecorder.tsx @@ -1,6 +1,7 @@ import React, { useState, useRef } from 'react'; import { Mic, Square, Loader, Play, Upload } from 'lucide-react'; import { supabase } from '../../lib/supabase'; +import { v4 as uuidv4 } from 'uuid'; interface AudioRecorderProps { storyId: string; @@ -51,6 +52,29 @@ export function AudioRecorder({ storyId, studentId, onAudioUploaded }: AudioReco } }; + const triggerAudioProcessing = async (recordingData: { + id: string; + story_id: string; + student_id: string; + audio_url: string; + status: string; + }): Promise => { + try { + const { error } = await supabase.functions.invoke('process-audio', { + body: { + record: recordingData + } + }); + + if (error) { + console.error('Erro ao iniciar processamento:', error); + // Não vamos tratar o erro aqui pois o processamento é assíncrono + } + } catch (err) { + console.error('Erro ao chamar função de processamento:', err); + } + }; + const uploadAudio = async () => { if (!audioBlob) return; @@ -63,52 +87,75 @@ export function AudioRecorder({ storyId, studentId, onAudioUploaded }: AudioReco setIsUploading(true); setError(null); + // Gerar um UUID único para o arquivo + const fileId = uuidv4(); + const filePath = `${studentId}/${storyId}/${fileId}.webm`; + try { - // 1. Primeiro criar o registro no banco + // Iniciar uma transação const { data: recordData, error: recordError } = await supabase .from('story_recordings') .insert({ + id: fileId, // Usar o mesmo UUID como ID do registro story_id: storyId, student_id: studentId, - status: 'pending_analysis', + status: 'uploading', // Status inicial created_at: new Date().toISOString() }) - .select() + .select('id') .single(); if (recordError) throw recordError; - if (!recordData) throw new Error('Registro não criado'); - // 2. Upload do arquivo usando o ID do registro - const filePath = `${studentId}/${storyId}/${recordData.id}.webm`; - + // Upload do arquivo const { error: uploadError } = await supabase.storage - .from('recordings') // Mudado para audio-uploads para manter consistência + .from('recordings') .upload(filePath, audioBlob, { contentType: 'audio/webm', - cacheControl: '3600' + cacheControl: '3600', + upsert: false }); if (uploadError) { - // Limpar registro se upload falhar - await supabase.from('story_recordings').delete().eq('id', recordData.id); + // Se o upload falhar, remover o registro do banco + await supabase + .from('story_recordings') + .delete() + .eq('id', fileId); throw uploadError; } - // 3. Obter URL pública + // Obter URL pública const { data: { publicUrl } } = supabase.storage .from('recordings') .getPublicUrl(filePath); - // 4. Atualizar registro com URL + // Atualizar o registro com a URL e status const { error: updateError } = await supabase .from('story_recordings') .update({ - audio_url: publicUrl + audio_url: publicUrl, + status: 'pending_analysis' }) - .eq('id', recordData.id); + .eq('id', fileId); - if (updateError) throw updateError; + if (updateError) { + // Se a atualização falhar, limpar tudo + await Promise.all([ + supabase.storage.from('recordings').remove([filePath]), + supabase.from('story_recordings').delete().eq('id', fileId) + ]); + throw updateError; + } + + // Disparar o processamento de forma assíncrona + triggerAudioProcessing({ + id: fileId, + story_id: storyId, + student_id: studentId, + audio_url: publicUrl, + status: 'pending_analysis' + }).catch(console.error); // Capturar erros mas não esperar pela conclusão onAudioUploaded(publicUrl); setAudioBlob(null);