import { serve } from 'https://deno.land/std@0.168.0/http/server.ts' import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' import { processAudioWithWhisper } from './whisper.ts' import { analyzeReading } from './analyzer.ts' import { createLogger } from './logger.ts' const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', 'Cross-Origin-Resource-Policy': 'cross-origin' } interface AudioRecord { id: string story_id: string student_id: string audio_url: string status: 'pending_analysis' | 'processing' | 'completed' | 'error' analysis: any created_at: string transcription: string | null processed_at: string | null error_message: string | null fluency_score: number | null pronunciation_score: number | null accuracy_score: number | null comprehension_score: number | null words_per_minute: number | null pause_count: number | null error_count: number | null self_corrections: number | null strengths: string[] improvements: string[] suggestions: string | null } // Função principal de processamento async function processAudioRecord(audioRecord: AudioRecord, logger: any) { const supabase = createClient( Deno.env.get('SUPABASE_URL') ?? '', Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '' ) // 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', processed_at: new Date().toISOString() }) .eq('id', audioRecord.id) if (processingError) { logger.error('processing_status_error', processingError) 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) logger.info('transcription_complete', 'Transcrição concluída', { length: transcription?.length || 0 }) // 3. Análise do áudio const analysis = await analyzeReading(transcription, audioRecord.story_id, logger) logger.info('analysis_complete', 'Análise concluída', { scores: analysis }) // 4. Prepara dados para atualização const updateData = { status: 'completed', transcription: transcription || '', processed_at: new Date().toISOString(), fluency_score: Number(analysis.fluency_score) || 0, pronunciation_score: Number(analysis.pronunciation_score) || 0, accuracy_score: Number(analysis.accuracy_score) || 0, comprehension_score: Number(analysis.comprehension_score) || 0, words_per_minute: Number(analysis.words_per_minute) || 0, pause_count: Number(analysis.pause_count) || 0, error_count: Number(analysis.error_count) || 0, self_corrections: Number(analysis.self_corrections) || 0, strengths: Array.isArray(analysis.strengths) ? analysis.strengths : [], improvements: Array.isArray(analysis.improvements) ? analysis.improvements : [], suggestions: analysis.suggestions || '' } logger.info('update_start', 'Iniciando atualização no banco', updateData) // 5. Verifica se o registro existe antes do update const { data: preUpdateCheck, error: preCheckError } = await supabase .from('story_recordings') .select('id, status') .eq('id', audioRecord.id) .limit(1) if (preCheckError) { logger.error('pre_check_error', preCheckError) throw preCheckError } if (!preUpdateCheck || preUpdateCheck.length === 0) { const error = new Error(`Registro ${audioRecord.id} não encontrado antes do update`) logger.error('pre_update_check_error', error) throw error } logger.info('pre_update_check', 'Registro encontrado antes do update', preUpdateCheck[0]) // Realiza o update const { data: updateResult, error: updateError } = await supabase .from('story_recordings') .update(updateData) .eq('id', audioRecord.id) .select() if (updateError) { logger.error('update_error', updateError, { id: audioRecord.id }) throw updateError } logger.info('update_result', 'Resultado do update', { hasData: !!updateResult, dataLength: updateResult?.length || 0, updateData }) // Verifica novamente após o update const { data: verifyData, error: verifyError } = await supabase .from('story_recordings') .select('*') .eq('id', audioRecord.id) .limit(1) logger.info('verify_result', 'Resultado da verificação', { hasData: !!verifyData, dataLength: verifyData?.length || 0, firstRecord: verifyData?.[0]?.id }) if (verifyError) { logger.error('verify_error', verifyError) throw verifyError } if (!verifyData || verifyData.length === 0) { const error = new Error(`Registro ${audioRecord.id} não encontrado após atualização`) logger.error('update_validation_error', error) throw error } const updatedData = verifyData[0] logger.info('update_complete', 'Atualização concluída com sucesso', { success: true, updatedData }) return { analysis, updatedData } } // Handler principal da Edge Function serve(async (req) => { let logger: any = null let data: { record: AudioRecord } | null = null if (req.method === 'OPTIONS') { return new Response('ok', { headers: corsHeaders }) } try { data = await req.json() logger = createLogger(data?.record?.id || 'unknown') logger.info('request_received', 'Iniciando processamento', { headers: Object.fromEntries(req.headers.entries()) }) // Validação if (!data?.record?.id || !data?.record?.audio_url) { const error = new Error('Dados inválidos: ID ou URL do áudio ausentes') logger.error('validation_error', 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) return new Response( JSON.stringify({ message: 'Áudio processado com sucesso', data: result.analysis }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 200, } ) } catch (error) { logger?.error('processing_error', error) console.error('Erro ao processar áudio:', error) try { if (data?.record?.id) { const supabase = createClient( Deno.env.get('SUPABASE_URL') ?? '', Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '', { auth: { persistSession: false } } ) logger?.info('error_status_update_start', 'Atualizando status para error') await supabase .from('story_recordings') .update({ status: 'error', error_message: error.message || 'Erro desconhecido' }) .eq('id', data.record.id) } } catch (updateError) { logger?.error('error_status_update_failed', updateError) console.error('Erro ao atualizar registro com status de erro:', updateError) } return new Response( JSON.stringify({ error: 'Falha ao processar áudio', details: error.message || 'Erro desconhecido' }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 500, } ) } })