From 933358483ef9455ade57c8aeec91afc32dc5a571 Mon Sep 17 00:00:00 2001 From: Lucas Santana Date: Sat, 28 Dec 2024 12:42:26 -0300 Subject: [PATCH] =?UTF-8?q?Corrigindo=20processamento=20do=20=C3=A1udio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/audio/AudioUploader.tsx | 34 +++++----- src/components/story/AudioRecorder.tsx | 50 ++++++++++----- src/services/audioService.ts | 70 +++++++++++++++++---- supabase/functions/process-audio/index.ts | 60 +++++++++++++++++- supabase/functions/process-audio/whisper.ts | 34 +++++----- 5 files changed, 186 insertions(+), 62 deletions(-) diff --git a/src/components/audio/AudioUploader.tsx b/src/components/audio/AudioUploader.tsx index fcd458a..fa07f36 100644 --- a/src/components/audio/AudioUploader.tsx +++ b/src/components/audio/AudioUploader.tsx @@ -2,9 +2,18 @@ import React from 'react'; import { processAudio } from '../../services/audioService'; import { Button } from '../ui/button'; -export function AudioUploader(): JSX.Element { +interface AudioUploaderProps { + storyId: string; + onUploadComplete?: (transcription: string) => void; + onError?: (error: string) => void; +} + +export function AudioUploader({ + storyId, + onUploadComplete, + onError +}: AudioUploaderProps): JSX.Element { const [isProcessing, setIsProcessing] = React.useState(false); - const [transcription, setTranscription] = React.useState(); const [error, setError] = React.useState(); const handleFileUpload = async (event: React.ChangeEvent) => { @@ -15,16 +24,18 @@ export function AudioUploader(): JSX.Element { setIsProcessing(true); setError(undefined); - const response = await processAudio(file); + const response = await processAudio(file, storyId); if (response.error) { setError(response.error); - } else { - setTranscription(response.transcription); + onError?.(response.error); + } else if (response.transcription) { + onUploadComplete?.(response.transcription); } } catch (err) { - setError('Erro ao processar áudio. Tente novamente.'); - console.error(err); + const errorMessage = err instanceof Error ? err.message : 'Erro ao processar áudio'; + setError(errorMessage); + onError?.(errorMessage); } finally { setIsProcessing(false); } @@ -53,14 +64,7 @@ export function AudioUploader(): JSX.Element { {error && ( -

{error}

- )} - - {transcription && ( -
-

Transcrição:

-

{transcription}

-
+

{error}

)} ); diff --git a/src/components/story/AudioRecorder.tsx b/src/components/story/AudioRecorder.tsx index f4f8378..bf25dc8 100644 --- a/src/components/story/AudioRecorder.tsx +++ b/src/components/story/AudioRecorder.tsx @@ -64,39 +64,55 @@ export function AudioRecorder({ storyId, studentId, onAudioUploaded }: AudioReco setError(null); try { - // Criar nome único para o arquivo - const timestamp = new Date().getTime(); - const filePath = `${studentId}/${storyId}/${timestamp}.webm`; + // 1. Primeiro criar o registro no banco + const { data: recordData, error: recordError } = await supabase + .from('story_recordings') + .insert({ + story_id: storyId, + student_id: studentId, + status: 'pending_analysis', + created_at: new Date().toISOString() + }) + .select() + .single(); - // Upload do arquivo - const { data, error: uploadError } = await supabase.storage - .from('recordings') + if (recordError) throw recordError; + if (!recordData) throw new Error('Registro não criado'); + + // 2. Upload do arquivo usando o ID do registro + const filePath = `recordings/${recordData.id}/${Date.now()}.webm`; + + const { error: uploadError } = await supabase.storage + .from('recordings') // Mudado para audio-uploads para manter consistência .upload(filePath, audioBlob, { contentType: 'audio/webm', cacheControl: '3600' }); - if (uploadError) throw uploadError; + if (uploadError) { + // Limpar registro se upload falhar + await supabase.from('story_recordings').delete().eq('id', recordData.id); + throw uploadError; + } - // Obter URL pública + // 3. Obter URL pública const { data: { publicUrl } } = supabase.storage .from('recordings') .getPublicUrl(filePath); - // Salvar referência no banco apenas com os campos necessários - const { error: dbError } = await supabase + // 4. Atualizar registro com URL + const { error: updateError } = await supabase .from('story_recordings') - .insert({ - story_id: storyId, - student_id: studentId, - audio_url: publicUrl, - status: 'pending_analysis' - }); + .update({ + audio_url: publicUrl + }) + .eq('id', recordData.id); - if (dbError) throw dbError; + if (updateError) throw updateError; onAudioUploaded(publicUrl); setAudioBlob(null); + } catch (err) { setError('Erro ao enviar áudio. Tente novamente.'); console.error('Erro no upload:', err); diff --git a/src/services/audioService.ts b/src/services/audioService.ts index 154205e..3659455 100644 --- a/src/services/audioService.ts +++ b/src/services/audioService.ts @@ -5,23 +5,71 @@ interface ProcessAudioResponse { error?: string; } -export async function processAudio(audioFile: File): Promise { +export async function processAudio(audioFile: File, storyId: string): Promise { try { - // 1. Upload do arquivo para o bucket do Supabase - const fileName = `audio-${Date.now()}-${audioFile.name}`; + // 1. Gerar nome único para o arquivo + const fileName = `${crypto.randomUUID()}-${audioFile.name}`; + + // 2. Primeiro criar o registro no banco + const { data: recordData, error: recordError } = await supabase + .from('story_recordings') + .insert({ + story_id: storyId, + status: 'pending_analysis', + created_at: new Date().toISOString() + }) + .select() + .single(); + + if (recordError) throw recordError; + + // 3. Upload do arquivo para o bucket do Supabase const { data: uploadData, error: uploadError } = await supabase.storage .from('audio-uploads') - .upload(fileName, audioFile); + .upload(`recordings/${recordData.id}/${fileName}`, audioFile, { + cacheControl: '3600', + contentType: audioFile.type, + upsert: false + }); - if (uploadError) throw uploadError; + if (uploadError) { + // Se falhar o upload, deletar o registro + await supabase + .from('story_recordings') + .delete() + .eq('id', recordData.id); + + throw uploadError; + } - // 2. Chama a Edge Function para processar o áudio - const { data, error } = await supabase.functions.invoke('process-audio', { - body: { - fileName: uploadData.path, - bucket: 'audio-uploads' + // 4. Pegar URL pública do arquivo + const { data: { publicUrl } } = supabase.storage + .from('audio-uploads') + .getPublicUrl(`recordings/${recordData.id}/${fileName}`); + + // 5. Atualizar registro com URL do áudio + const { error: updateError } = await supabase + .from('story_recordings') + .update({ + audio_url: publicUrl + }) + .eq('id', recordData.id); + + if (updateError) throw updateError; + + // 6. Chamar a Edge Function para processar o áudio + const { data, error } = await supabase.functions.invoke( + 'process-audio', + { + body: { + record: { + id: recordData.id, + story_id: storyId, + audio_url: publicUrl + } + } } - }); + ); if (error) throw error; diff --git a/supabase/functions/process-audio/index.ts b/supabase/functions/process-audio/index.ts index cdc0f63..d06f1d7 100644 --- a/supabase/functions/process-audio/index.ts +++ b/supabase/functions/process-audio/index.ts @@ -40,11 +40,56 @@ async function processAudioRecord(audioRecord: AudioRecord, logger: any) { Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '' ) - // 1. Atualiza status para processing - logger.info('status_update_start', 'Atualizando status para processing') + // 1. Verifica e atualiza status para processing + logger.info('status_update_start', 'Verificando e atualizando registro') + + // Primeiro verifica se o registro existe + const { data: existingRecord, error: checkError } = await supabase + .from('story_recordings') + .select('id, status') + .eq('id', audioRecord.id) + .maybeSingle() + + logger.info('query_debug', 'Resultado da query', { + hasData: !!existingRecord, + recordId: audioRecord.id, + record: existingRecord, + error: checkError?.message + }) + + if (checkError) { + logger.error('check_error', checkError) + throw new Error(`Erro ao verificar registro: ${checkError.message}`) + } + + if (!existingRecord) { + // Se não existe, tenta criar + logger.info('record_create', 'Tentando criar registro', audioRecord) + + const { error: insertError } = await supabase + .from('story_recordings') + .insert({ + id: audioRecord.id, + story_id: audioRecord.story_id, + student_id: audioRecord.student_id, + audio_url: audioRecord.audio_url, + status: 'pending_analysis', + created_at: new Date().toISOString() + }) + + if (insertError) { + logger.error('record_create_error', insertError) + throw new Error(`Erro ao criar registro: ${insertError.message}`) + } + } + + // Atualiza para processing const { error: processingError } = await supabase .from('story_recordings') - .update({ status: 'processing' }) + .update({ + status: 'processing', + processed_at: new Date().toISOString() + }) .eq('id', audioRecord.id) if (processingError) { @@ -52,6 +97,8 @@ async function processAudioRecord(audioRecord: AudioRecord, logger: any) { throw processingError } + logger.info('status_updated', 'Status atualizado para processing', { id: audioRecord.id }) + // 2. Processamento do áudio logger.info('processing_start', 'Iniciando processamento do áudio') const transcription = await processAudioWithWhisper(audioRecord.audio_url, logger) @@ -177,6 +224,13 @@ serve(async (req) => { throw error } + logger.info('id_debug', 'Verificando ID', { + id: data.record.id, + idType: typeof data.record.id, + idLength: data.record.id.length, + isValidUUID: /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(data.record.id) + }); + const result = await processAudioRecord(data.record, logger) return new Response( diff --git a/supabase/functions/process-audio/whisper.ts b/supabase/functions/process-audio/whisper.ts index a46e197..6756aad 100644 --- a/supabase/functions/process-audio/whisper.ts +++ b/supabase/functions/process-audio/whisper.ts @@ -5,13 +5,13 @@ const openai = new OpenAI({ apiKey: Deno.env.get('OPENAI_API_KEY') }) -export async function processAudioWithWhisper(audioUrl: string, logger: Logger): Promise { +export async function processAudioWithWhisper(audioUrl: string, logger: any): Promise { try { logger.info('whisper_start', 'Iniciando download do áudio', { url: audioUrl }) const audioResponse = await fetch(audioUrl) if (!audioResponse.ok) { - logger.error('whisper_download', new Error(`HTTP ${audioResponse.status}`)) + logger.error('whisper_download_error', new Error(`HTTP ${audioResponse.status}`)) throw new Error('Falha ao baixar áudio') } @@ -21,15 +21,13 @@ export async function processAudioWithWhisper(audioUrl: string, logger: Logger): type: audioBlob.type }) - // Debug do áudio - console.log('Audio blob:', { - size: audioBlob.size, - type: audioBlob.type - }) + // Validação do blob + if (audioBlob.size === 0) { + throw new Error('Arquivo de áudio vazio') + } - // Converte Blob para File - const audioFile = new File([audioBlob], 'audio.mp3', { - type: audioBlob.type || 'audio/mpeg' + const audioFile = new File([audioBlob], 'audio.webm', { + type: audioBlob.type || 'audio/webm' }) // Transcrição com Whisper @@ -37,18 +35,22 @@ export async function processAudioWithWhisper(audioUrl: string, logger: Logger): file: audioFile, model: 'whisper-1', language: 'pt' - // removido response_format pois estava causando erro }) - console.log('Transcription response:', transcription) // Debug - - if (!transcription || typeof transcription === 'undefined') { + if (!transcription?.text) { throw new Error('Resposta da transcrição vazia') } - return transcription.text || '' + logger.info('whisper_complete', 'Transcrição concluída', { + length: transcription.text.length + }) + + return transcription.text + } catch (error) { - console.error('Erro detalhado na transcrição:', error) + logger.error('whisper_error', error, { + url: audioUrl + }) throw error } } \ No newline at end of file