mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-17 05:47:52 +00:00
Corrigindo processamento do áudio
This commit is contained in:
parent
66d401f98f
commit
933358483e
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user