Corrigindo processamento do áudio

This commit is contained in:
Lucas Santana 2024-12-28 12:42:26 -03:00
parent 66d401f98f
commit 933358483e
5 changed files with 186 additions and 62 deletions

View File

@ -2,9 +2,18 @@ import React from 'react';
import { processAudio } from '../../services/audioService'; import { processAudio } from '../../services/audioService';
import { Button } from '../ui/button'; 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 [isProcessing, setIsProcessing] = React.useState(false);
const [transcription, setTranscription] = React.useState<string>();
const [error, setError] = React.useState<string>(); const [error, setError] = React.useState<string>();
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => { const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
@ -15,16 +24,18 @@ export function AudioUploader(): JSX.Element {
setIsProcessing(true); setIsProcessing(true);
setError(undefined); setError(undefined);
const response = await processAudio(file); const response = await processAudio(file, storyId);
if (response.error) { if (response.error) {
setError(response.error); setError(response.error);
} else { onError?.(response.error);
setTranscription(response.transcription); } else if (response.transcription) {
onUploadComplete?.(response.transcription);
} }
} catch (err) { } catch (err) {
setError('Erro ao processar áudio. Tente novamente.'); const errorMessage = err instanceof Error ? err.message : 'Erro ao processar áudio';
console.error(err); setError(errorMessage);
onError?.(errorMessage);
} finally { } finally {
setIsProcessing(false); setIsProcessing(false);
} }
@ -53,14 +64,7 @@ export function AudioUploader(): JSX.Element {
</div> </div>
{error && ( {error && (
<p className="text-red-500 text-sm">{error}</p> <p className="text-sm text-red-600">{error}</p>
)}
{transcription && (
<div className="p-4 bg-gray-50 rounded-lg">
<h3 className="font-medium mb-2">Transcrição:</h3>
<p>{transcription}</p>
</div>
)} )}
</div> </div>
); );

View File

@ -64,39 +64,55 @@ export function AudioRecorder({ storyId, studentId, onAudioUploaded }: AudioReco
setError(null); setError(null);
try { try {
// Criar nome único para o arquivo // 1. Primeiro criar o registro no banco
const timestamp = new Date().getTime(); const { data: recordData, error: recordError } = await supabase
const filePath = `${studentId}/${storyId}/${timestamp}.webm`; .from('story_recordings')
.insert({
story_id: storyId,
student_id: studentId,
status: 'pending_analysis',
created_at: new Date().toISOString()
})
.select()
.single();
// Upload do arquivo if (recordError) throw recordError;
const { data, error: uploadError } = await supabase.storage if (!recordData) throw new Error('Registro não criado');
.from('recordings')
// 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, { .upload(filePath, audioBlob, {
contentType: 'audio/webm', contentType: 'audio/webm',
cacheControl: '3600' 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 const { data: { publicUrl } } = supabase.storage
.from('recordings') .from('recordings')
.getPublicUrl(filePath); .getPublicUrl(filePath);
// Salvar referência no banco apenas com os campos necessários // 4. Atualizar registro com URL
const { error: dbError } = await supabase const { error: updateError } = await supabase
.from('story_recordings') .from('story_recordings')
.insert({ .update({
story_id: storyId, audio_url: publicUrl
student_id: studentId, })
audio_url: publicUrl, .eq('id', recordData.id);
status: 'pending_analysis'
});
if (dbError) throw dbError; if (updateError) throw updateError;
onAudioUploaded(publicUrl); onAudioUploaded(publicUrl);
setAudioBlob(null); setAudioBlob(null);
} catch (err) { } catch (err) {
setError('Erro ao enviar áudio. Tente novamente.'); setError('Erro ao enviar áudio. Tente novamente.');
console.error('Erro no upload:', err); console.error('Erro no upload:', err);

View File

@ -5,24 +5,72 @@ interface ProcessAudioResponse {
error?: string; error?: string;
} }
export async function processAudio(audioFile: File): Promise<ProcessAudioResponse> { export async function processAudio(audioFile: File, storyId: string): Promise<ProcessAudioResponse> {
try { try {
// 1. Upload do arquivo para o bucket do Supabase // 1. Gerar nome único para o arquivo
const fileName = `audio-${Date.now()}-${audioFile.name}`; 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 const { data: uploadData, error: uploadError } = await supabase.storage
.from('audio-uploads') .from('audio-uploads')
.upload(fileName, audioFile); .upload(`recordings/${recordData.id}/${fileName}`, audioFile, {
cacheControl: '3600',
if (uploadError) throw uploadError; contentType: audioFile.type,
upsert: false
// 2. Chama a Edge Function para processar o áudio
const { data, error } = await supabase.functions.invoke<ProcessAudioResponse>('process-audio', {
body: {
fileName: uploadData.path,
bucket: 'audio-uploads'
}
}); });
if (uploadError) {
// Se falhar o upload, deletar o registro
await supabase
.from('story_recordings')
.delete()
.eq('id', recordData.id);
throw uploadError;
}
// 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<ProcessAudioResponse>(
'process-audio',
{
body: {
record: {
id: recordData.id,
story_id: storyId,
audio_url: publicUrl
}
}
}
);
if (error) throw error; if (error) throw error;
return { return {

View File

@ -40,11 +40,56 @@ async function processAudioRecord(audioRecord: AudioRecord, logger: any) {
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '' Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
) )
// 1. Atualiza status para processing // 1. Verifica e atualiza status para processing
logger.info('status_update_start', 'Atualizando 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 const { error: processingError } = await supabase
.from('story_recordings') .from('story_recordings')
.update({ status: 'processing' }) .update({
status: 'processing',
processed_at: new Date().toISOString()
})
.eq('id', audioRecord.id) .eq('id', audioRecord.id)
if (processingError) { if (processingError) {
@ -52,6 +97,8 @@ async function processAudioRecord(audioRecord: AudioRecord, logger: any) {
throw processingError throw processingError
} }
logger.info('status_updated', 'Status atualizado para processing', { id: audioRecord.id })
// 2. Processamento do áudio // 2. Processamento do áudio
logger.info('processing_start', 'Iniciando processamento do áudio') logger.info('processing_start', 'Iniciando processamento do áudio')
const transcription = await processAudioWithWhisper(audioRecord.audio_url, logger) const transcription = await processAudioWithWhisper(audioRecord.audio_url, logger)
@ -177,6 +224,13 @@ serve(async (req) => {
throw error 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) const result = await processAudioRecord(data.record, logger)
return new Response( return new Response(

View File

@ -5,13 +5,13 @@ const openai = new OpenAI({
apiKey: Deno.env.get('OPENAI_API_KEY') apiKey: Deno.env.get('OPENAI_API_KEY')
}) })
export async function processAudioWithWhisper(audioUrl: string, logger: Logger): Promise<string> { export async function processAudioWithWhisper(audioUrl: string, logger: any): Promise<string> {
try { try {
logger.info('whisper_start', 'Iniciando download do áudio', { url: audioUrl }) logger.info('whisper_start', 'Iniciando download do áudio', { url: audioUrl })
const audioResponse = await fetch(audioUrl) const audioResponse = await fetch(audioUrl)
if (!audioResponse.ok) { 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') throw new Error('Falha ao baixar áudio')
} }
@ -21,15 +21,13 @@ export async function processAudioWithWhisper(audioUrl: string, logger: Logger):
type: audioBlob.type type: audioBlob.type
}) })
// Debug do áudio // Validação do blob
console.log('Audio blob:', { if (audioBlob.size === 0) {
size: audioBlob.size, throw new Error('Arquivo de áudio vazio')
type: audioBlob.type }
})
// Converte Blob para File const audioFile = new File([audioBlob], 'audio.webm', {
const audioFile = new File([audioBlob], 'audio.mp3', { type: audioBlob.type || 'audio/webm'
type: audioBlob.type || 'audio/mpeg'
}) })
// Transcrição com Whisper // Transcrição com Whisper
@ -37,18 +35,22 @@ export async function processAudioWithWhisper(audioUrl: string, logger: Logger):
file: audioFile, file: audioFile,
model: 'whisper-1', model: 'whisper-1',
language: 'pt' language: 'pt'
// removido response_format pois estava causando erro
}) })
console.log('Transcription response:', transcription) // Debug if (!transcription?.text) {
if (!transcription || typeof transcription === 'undefined') {
throw new Error('Resposta da transcrição vazia') 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) { } catch (error) {
console.error('Erro detalhado na transcrição:', error) logger.error('whisper_error', error, {
url: audioUrl
})
throw error throw error
} }
} }