mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-16 21:37:51 +00:00
feat: adiciona processamento automático de áudio
- Implementa Edge Function para processamento de áudio - Adiciona integração com OpenAI Whisper e GPT-4 - Configura Database Trigger para story_recordings - Implementa análise automática de leitura - Atualiza documentação e variáveis de ambiente
This commit is contained in:
parent
4d09386d96
commit
a8c332d442
2
.gitignore
vendored
2
.gitignore
vendored
@ -26,6 +26,8 @@ dist-ssr
|
|||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
.env*
|
||||||
|
.env.*
|
||||||
|
|
||||||
# Backup files
|
# Backup files
|
||||||
*copy*
|
*copy*
|
||||||
|
|||||||
55
CHANGELOG.md
55
CHANGELOG.md
@ -1,52 +1,19 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
Todas as mudanças notáveis neste projeto serão documentadas neste arquivo.
|
## [1.1.0] - 2024-01-27
|
||||||
|
|
||||||
O formato é baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.0.0/),
|
|
||||||
e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/).
|
|
||||||
|
|
||||||
## [0.2.0] - 2024-03-21
|
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
- Configuração Docker para ambiente de produção
|
- Função de processamento de áudio (process-audio)
|
||||||
- Pipeline de CI/CD no Gitea Actions
|
- Integração com OpenAI Whisper para transcrição
|
||||||
- Integração com Redis para cache
|
- Análise de leitura usando GPT-4
|
||||||
- Healthcheck da aplicação
|
- Database Trigger para processamento automático de gravações
|
||||||
- Adiciona seções:
|
|
||||||
- Hero com CTA e social proof
|
|
||||||
- Problemas e Soluções
|
|
||||||
- Como a Magia Acontece
|
|
||||||
- Comparação antes/depois
|
|
||||||
- Benefícios Mágicos em layout horizontal
|
|
||||||
- Testimoniais
|
|
||||||
- Planos e preços
|
|
||||||
- FAQ
|
|
||||||
- CTA final
|
|
||||||
- Footer
|
|
||||||
|
|
||||||
### Técnico
|
### Técnico
|
||||||
- Dockerfile com multi-stage build para otimização
|
- Implementação de Edge Function no Supabase
|
||||||
- Configuração de registry no Gitea
|
- Configuração de ambiente de desenvolvimento com Docker
|
||||||
- Cache de histórias com Redis
|
- Integração com banco de dados para story_recordings
|
||||||
- Implementação de cliente Redis com:
|
- Sistema de análise de métricas de leitura
|
||||||
- Retry strategy otimizada
|
|
||||||
- Reconexão automática
|
|
||||||
- Tipagem forte
|
|
||||||
- Funções utilitárias para cache
|
|
||||||
- Scripts de deploy e monitoramento
|
|
||||||
|
|
||||||
### Modificado
|
### Modificado
|
||||||
- Atualização do next.config.js para suporte standalone
|
- Estrutura de armazenamento de gravações
|
||||||
- Adaptação da API para usar Redis cache
|
- Fluxo de processamento de áudio automatizado
|
||||||
- Configuração de redes Docker
|
|
||||||
- Otimiza UX/UI com:
|
|
||||||
- Animações suaves
|
|
||||||
- Gradientes modernos
|
|
||||||
- Layout responsivo
|
|
||||||
- Elementos interativos
|
|
||||||
- Social proof estratégico
|
|
||||||
|
|
||||||
### Segurança
|
|
||||||
- Implementação de healthchecks
|
|
||||||
- Configuração de redes isoladas
|
|
||||||
- Proteção de variáveis de ambiente
|
|
||||||
15
Dockerfile.dev
Normal file
15
Dockerfile.dev
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Adicionar dependência do Redis
|
||||||
|
RUN apk add --no-cache redis
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
COPY yarn.lock ./
|
||||||
|
|
||||||
|
RUN yarn install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
CMD ["yarn", "dev"]
|
||||||
31
docker-compose.dev.yml
Normal file
31
docker-compose.dev.yml
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- /app/node_modules
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=development
|
||||||
|
- NEXT_PUBLIC_SUPABASE_URL=${NEXT_PUBLIC_SUPABASE_URL}
|
||||||
|
- NEXT_PUBLIC_SUPABASE_ANON_KEY=${NEXT_PUBLIC_SUPABASE_ANON_KEY}
|
||||||
|
- SUPABASE_SERVICE_ROLE_KEY=${SUPABASE_SERVICE_ROLE_KEY}
|
||||||
|
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||||
|
- REDIS_URL=redis://redis:6379
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:alpine
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
redis_data:
|
||||||
@ -27,7 +27,5 @@ services:
|
|||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
traefik-public:
|
network_public:
|
||||||
external: true
|
external: true
|
||||||
redis-network:
|
|
||||||
external: true
|
|
||||||
169
supabase/functions/process-audio/analyzer.ts
Normal file
169
supabase/functions/process-audio/analyzer.ts
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import { OpenAI } from 'https://deno.land/x/openai@v4.20.1/mod.ts'
|
||||||
|
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
|
||||||
|
|
||||||
|
const openai = new OpenAI({
|
||||||
|
apiKey: Deno.env.get('OPENAI_API_KEY')
|
||||||
|
})
|
||||||
|
|
||||||
|
interface ReadingAnalysis {
|
||||||
|
fluency_score: number
|
||||||
|
pronunciation_score: number
|
||||||
|
accuracy_score: number
|
||||||
|
comprehension_score: number
|
||||||
|
words_per_minute: number
|
||||||
|
pause_count: number
|
||||||
|
error_count: number
|
||||||
|
self_corrections: number
|
||||||
|
strengths: string[]
|
||||||
|
improvements: string[]
|
||||||
|
suggestions: string
|
||||||
|
raw_data: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function analyzeReading(
|
||||||
|
transcription: string,
|
||||||
|
storyId: string
|
||||||
|
): Promise<ReadingAnalysis> {
|
||||||
|
try {
|
||||||
|
console.log('Analisando leitura para story_id:', storyId)
|
||||||
|
|
||||||
|
const supabase = createClient(
|
||||||
|
Deno.env.get('SUPABASE_URL') ?? '',
|
||||||
|
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
|
||||||
|
)
|
||||||
|
|
||||||
|
// Busca a história com o texto da página
|
||||||
|
const { data: storyPages, error: storyError } = await supabase
|
||||||
|
.from('story_pages')
|
||||||
|
.select('text')
|
||||||
|
.eq('story_id', storyId)
|
||||||
|
.order('page_number', { ascending: true })
|
||||||
|
|
||||||
|
if (storyError) {
|
||||||
|
console.error('Erro ao buscar história:', storyError)
|
||||||
|
throw storyError
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!storyPages || storyPages.length === 0) {
|
||||||
|
console.error('Dados da história inválidos:', storyPages)
|
||||||
|
throw new Error('Texto da história não encontrado')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concatena todos os textos das páginas
|
||||||
|
const originalText = storyPages.map(page => page.text).join(' ')
|
||||||
|
console.log('Texto original:', originalText)
|
||||||
|
console.log('Transcrição:', transcription)
|
||||||
|
|
||||||
|
// Análise com GPT-4
|
||||||
|
const analysis = await openai.chat.completions.create({
|
||||||
|
model: "gpt-4o-mini",
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "system",
|
||||||
|
content: `Você é um especialista em análise de leitura infantil.
|
||||||
|
Analise a transcrição comparando com o texto original.
|
||||||
|
Forneça métricas detalhadas e feedback construtivo.
|
||||||
|
Retorne apenas o JSON solicitado, sem texto adicional.`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: `
|
||||||
|
Texto Original: "${originalText}"
|
||||||
|
Transcrição: "${transcription}"
|
||||||
|
|
||||||
|
Analise e retorne as métricas no formato JSON.`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
response_format: { type: "json_object" },
|
||||||
|
temperature: 0.7,
|
||||||
|
max_tokens: 1000,
|
||||||
|
functions: [
|
||||||
|
{
|
||||||
|
name: "analyze_reading",
|
||||||
|
description: "Analisa a leitura e retorna métricas",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
fluency_score: {
|
||||||
|
type: "number",
|
||||||
|
description: "Pontuação de fluência (0-100)"
|
||||||
|
},
|
||||||
|
pronunciation_score: {
|
||||||
|
type: "number",
|
||||||
|
description: "Pontuação de pronúncia (0-100)"
|
||||||
|
},
|
||||||
|
accuracy_score: {
|
||||||
|
type: "number",
|
||||||
|
description: "Pontuação de precisão (0-100)"
|
||||||
|
},
|
||||||
|
comprehension_score: {
|
||||||
|
type: "number",
|
||||||
|
description: "Pontuação de compreensão (0-100)"
|
||||||
|
},
|
||||||
|
words_per_minute: {
|
||||||
|
type: "number",
|
||||||
|
description: "Palavras por minuto"
|
||||||
|
},
|
||||||
|
pause_count: {
|
||||||
|
type: "number",
|
||||||
|
description: "Número de pausas"
|
||||||
|
},
|
||||||
|
error_count: {
|
||||||
|
type: "number",
|
||||||
|
description: "Número de erros"
|
||||||
|
},
|
||||||
|
self_corrections: {
|
||||||
|
type: "number",
|
||||||
|
description: "Número de autocorreções"
|
||||||
|
},
|
||||||
|
strengths: {
|
||||||
|
type: "array",
|
||||||
|
items: { type: "string" },
|
||||||
|
description: "3-5 pontos fortes"
|
||||||
|
},
|
||||||
|
improvements: {
|
||||||
|
type: "array",
|
||||||
|
items: { type: "string" },
|
||||||
|
description: "3-5 pontos para melhorar"
|
||||||
|
},
|
||||||
|
suggestions: {
|
||||||
|
type: "string",
|
||||||
|
description: "Sugestão personalizada"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: [
|
||||||
|
"fluency_score",
|
||||||
|
"pronunciation_score",
|
||||||
|
"accuracy_score",
|
||||||
|
"comprehension_score",
|
||||||
|
"words_per_minute",
|
||||||
|
"pause_count",
|
||||||
|
"error_count",
|
||||||
|
"self_corrections",
|
||||||
|
"strengths",
|
||||||
|
"improvements",
|
||||||
|
"suggestions"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!analysis.choices[0]?.message?.function_call?.arguments) {
|
||||||
|
throw new Error('Análise vazia do GPT')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Resposta do GPT:', analysis.choices[0].message.function_call.arguments)
|
||||||
|
|
||||||
|
const result = JSON.parse(analysis.choices[0].message.function_call.arguments)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
raw_data: analysis
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro detalhado na análise:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
44
supabase/functions/process-audio/hooks.ts
Normal file
44
supabase/functions/process-audio/hooks.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
|
||||||
|
import { processAudio } from './index.ts'
|
||||||
|
|
||||||
|
interface WebhookPayload {
|
||||||
|
type: 'INSERT' | 'UPDATE' | 'DELETE'
|
||||||
|
table: string
|
||||||
|
schema: string
|
||||||
|
record: {
|
||||||
|
id: string
|
||||||
|
story_id: string
|
||||||
|
student_id: string
|
||||||
|
audio_url: string
|
||||||
|
status: string
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
old_record: null | Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
serve(async (req) => {
|
||||||
|
try {
|
||||||
|
const payload: WebhookPayload = await req.json()
|
||||||
|
|
||||||
|
// Verifica se é uma inserção em story_recordings
|
||||||
|
if (
|
||||||
|
payload.type === 'INSERT' &&
|
||||||
|
payload.table === 'story_recordings' &&
|
||||||
|
payload.schema === 'public' &&
|
||||||
|
payload.record.status === 'pending_analysis'
|
||||||
|
) {
|
||||||
|
await processAudio(payload.record)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({ success: true }), {
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Hook error:', error)
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: error.message }),
|
||||||
|
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -1,127 +1,153 @@
|
|||||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
|
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
|
||||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
|
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
|
||||||
import { Configuration, OpenAIApi } from 'https://esm.sh/openai@3.1.0'
|
import { processAudioWithWhisper } from './whisper.ts'
|
||||||
|
import { analyzeReading } from './analyzer.ts'
|
||||||
|
|
||||||
// Configurar OpenAI
|
const corsHeaders = {
|
||||||
const openaiConfig = new Configuration({
|
'Access-Control-Allow-Origin': '*',
|
||||||
apiKey: Deno.env.get('OPENAI_API_KEY')
|
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||||
})
|
}
|
||||||
const openai = new OpenAIApi(openaiConfig)
|
|
||||||
|
|
||||||
// Configurar Supabase
|
interface AudioRecord {
|
||||||
const supabaseUrl = Deno.env.get('SUPABASE_URL')
|
id: string
|
||||||
const supabaseAnonKey = Deno.env.get('SUPABASE_ANON_KEY')
|
story_id: string
|
||||||
const supabase = createClient(supabaseUrl, supabaseAnonKey)
|
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
|
||||||
|
}
|
||||||
|
|
||||||
serve(async (req) => {
|
serve(async (req) => {
|
||||||
try {
|
if (req.method === 'OPTIONS') {
|
||||||
// Extrair dados do webhook
|
return new Response('ok', { headers: corsHeaders })
|
||||||
const { record } = await req.json()
|
}
|
||||||
const { id, audio_url, story_id } = record
|
|
||||||
|
|
||||||
// Atualizar status para processing
|
let data: any = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Log dos headers para debug
|
||||||
|
console.log('Headers:', Object.fromEntries(req.headers.entries()))
|
||||||
|
|
||||||
|
// Validação inicial do body
|
||||||
|
const body = await req.text()
|
||||||
|
console.log('Raw body:', body)
|
||||||
|
console.log('Body length:', body.length)
|
||||||
|
console.log('Body type:', typeof body)
|
||||||
|
|
||||||
|
data = JSON.parse(body)
|
||||||
|
|
||||||
|
// Validação do record
|
||||||
|
if (!data.record || !data.record.id || !data.record.audio_url) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
error: 'Payload inválido',
|
||||||
|
details: 'record, id e audio_url são obrigatórios',
|
||||||
|
receivedData: data
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 400,
|
||||||
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = createClient(
|
||||||
|
Deno.env.get('SUPABASE_URL') ?? '',
|
||||||
|
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
|
||||||
|
)
|
||||||
|
|
||||||
|
const audioRecord = data.record as AudioRecord
|
||||||
|
|
||||||
|
// 1. Atualiza status para processing
|
||||||
await supabase
|
await supabase
|
||||||
.from('story_recordings')
|
.from('story_recordings')
|
||||||
.update({ status: 'processing' })
|
.update({ status: 'processing' })
|
||||||
.eq('id', id)
|
.eq('id', audioRecord.id)
|
||||||
|
|
||||||
// Buscar texto original da história
|
// 2. Processa o áudio com Whisper
|
||||||
const { data: storyData } = await supabase
|
const transcription = await processAudioWithWhisper(audioRecord.audio_url)
|
||||||
.from('stories')
|
|
||||||
.select('content')
|
|
||||||
.eq('id', story_id)
|
|
||||||
.single()
|
|
||||||
|
|
||||||
const originalText = storyData.content.pages[0].text
|
// 3. Analisa a leitura
|
||||||
|
const analysis = await analyzeReading(transcription, audioRecord.story_id)
|
||||||
|
|
||||||
// 1. Transcrever áudio com Whisper
|
// 4. Atualiza o registro com os resultados
|
||||||
const audioResponse = await fetch(audio_url)
|
const { error: updateError } = await supabase
|
||||||
const audioBlob = await audioResponse.blob()
|
|
||||||
|
|
||||||
const transcription = await openai.createTranscription(
|
|
||||||
audioBlob,
|
|
||||||
'whisper-1',
|
|
||||||
'pt',
|
|
||||||
'verbose_json'
|
|
||||||
)
|
|
||||||
|
|
||||||
// 2. Analisar com GPT-4
|
|
||||||
const analysis = await openai.createChatCompletion({
|
|
||||||
model: "gpt-4",
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
role: "system",
|
|
||||||
content: `Você é um especialista em análise de leitura infantil.
|
|
||||||
Analise a transcrição comparando com o texto original.
|
|
||||||
Forneça uma análise detalhada em formato JSON com métricas de 0-100.`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: "user",
|
|
||||||
content: `
|
|
||||||
Texto Original: "${originalText}"
|
|
||||||
Transcrição: "${transcription.data.text}"
|
|
||||||
|
|
||||||
Analise e retorne um JSON com:
|
|
||||||
{
|
|
||||||
"metrics": {
|
|
||||||
"fluency": number,
|
|
||||||
"pronunciation": number,
|
|
||||||
"accuracy": number,
|
|
||||||
"comprehension": number
|
|
||||||
},
|
|
||||||
"feedback": {
|
|
||||||
"strengths": string[],
|
|
||||||
"improvements": string[],
|
|
||||||
"suggestions": string
|
|
||||||
},
|
|
||||||
"details": {
|
|
||||||
"wordsPerMinute": number,
|
|
||||||
"pauseCount": number,
|
|
||||||
"errorCount": number,
|
|
||||||
"selfCorrections": number
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
const analysisResult = JSON.parse(analysis.data.choices[0].message.content)
|
|
||||||
|
|
||||||
// 3. Atualizar registro com resultados
|
|
||||||
await supabase
|
|
||||||
.from('story_recordings')
|
.from('story_recordings')
|
||||||
.update({
|
.update({
|
||||||
transcription: transcription.data.text,
|
status: 'completed',
|
||||||
metrics: analysisResult.metrics,
|
transcription,
|
||||||
feedback: analysisResult.feedback,
|
processed_at: new Date().toISOString(),
|
||||||
details: analysisResult.details,
|
fluency_score: analysis.fluency_score,
|
||||||
status: 'analyzed',
|
pronunciation_score: analysis.pronunciation_score,
|
||||||
processed_at: new Date().toISOString()
|
accuracy_score: analysis.accuracy_score,
|
||||||
|
comprehension_score: analysis.comprehension_score,
|
||||||
|
words_per_minute: analysis.words_per_minute,
|
||||||
|
pause_count: analysis.pause_count,
|
||||||
|
error_count: analysis.error_count,
|
||||||
|
self_corrections: analysis.self_corrections,
|
||||||
|
strengths: analysis.strengths,
|
||||||
|
improvements: analysis.improvements,
|
||||||
|
suggestions: analysis.suggestions,
|
||||||
|
analysis: analysis.raw_data
|
||||||
})
|
})
|
||||||
.eq('id', id)
|
.eq('id', audioRecord.id)
|
||||||
|
|
||||||
|
if (updateError) throw updateError
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ success: true }),
|
JSON.stringify({
|
||||||
{ headers: { 'Content-Type': 'application/json' } }
|
message: 'Áudio processado com sucesso',
|
||||||
|
data: analysis
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||||
|
status: 200,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erro:', error)
|
console.error('Erro ao processar áudio:', error)
|
||||||
|
|
||||||
|
// Só tenta atualizar o registro se tiver o ID
|
||||||
|
if (data?.record?.id) {
|
||||||
|
const supabase = createClient(
|
||||||
|
Deno.env.get('SUPABASE_URL') ?? '',
|
||||||
|
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
|
||||||
|
)
|
||||||
|
|
||||||
// Atualizar registro com erro
|
|
||||||
if (error.record?.id) {
|
|
||||||
await supabase
|
await supabase
|
||||||
.from('story_recordings')
|
.from('story_recordings')
|
||||||
.update({
|
.update({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
error_message: error.message
|
error_message: error.message
|
||||||
})
|
})
|
||||||
.eq('id', error.record.id)
|
.eq('id', data.record.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ error: error.message }),
|
JSON.stringify({
|
||||||
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
error: 'Falha ao processar áudio',
|
||||||
|
details: error.message
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||||
|
status: 500,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
46
supabase/functions/process-audio/whisper.ts
Normal file
46
supabase/functions/process-audio/whisper.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { OpenAI } from 'https://deno.land/x/openai@v4.20.1/mod.ts'
|
||||||
|
|
||||||
|
const openai = new OpenAI({
|
||||||
|
apiKey: Deno.env.get('OPENAI_API_KEY')
|
||||||
|
})
|
||||||
|
|
||||||
|
export async function processAudioWithWhisper(audioUrl: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
// Download do áudio
|
||||||
|
const audioResponse = await fetch(audioUrl)
|
||||||
|
if (!audioResponse.ok) {
|
||||||
|
throw new Error('Falha ao baixar áudio')
|
||||||
|
}
|
||||||
|
const audioBlob = await audioResponse.blob()
|
||||||
|
|
||||||
|
// Debug do áudio
|
||||||
|
console.log('Audio blob:', {
|
||||||
|
size: audioBlob.size,
|
||||||
|
type: audioBlob.type
|
||||||
|
})
|
||||||
|
|
||||||
|
// Converte Blob para File
|
||||||
|
const audioFile = new File([audioBlob], 'audio.mp3', {
|
||||||
|
type: audioBlob.type || 'audio/mpeg'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Transcrição com Whisper
|
||||||
|
const transcription = await openai.audio.transcriptions.create({
|
||||||
|
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') {
|
||||||
|
throw new Error('Resposta da transcrição vazia')
|
||||||
|
}
|
||||||
|
|
||||||
|
return transcription.text || ''
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro detalhado na transcrição:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user