mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-18 22:37:51 +00:00
Compare commits
67 Commits
de28dea3b5
...
618ecaf040
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
618ecaf040 | ||
|
|
ee222a669e | ||
|
|
fa02f92839 | ||
|
|
31eff4610d | ||
|
|
f0538d82d3 | ||
|
|
6ae26361d9 | ||
|
|
f5d357f94b | ||
|
|
c50e28df01 | ||
|
|
34b93ddc3e | ||
|
|
f4761c2d28 | ||
|
|
879b2f7edf | ||
|
|
dec1bd0df3 | ||
|
|
5bc5b52939 | ||
|
|
18477021b5 | ||
|
|
a952e49c32 | ||
|
|
8948f33d2a | ||
|
|
39ae412e9b | ||
|
|
f523fdaece | ||
|
|
d1d91d39b4 | ||
|
|
db38deb0fa | ||
|
|
8475babb2c | ||
|
|
92e4546ecf | ||
|
|
f3d186cefa | ||
|
|
41adddabf3 | ||
|
|
a9d863f4a1 | ||
|
|
51e18dd06a | ||
|
|
97c9c0160b | ||
|
|
ae625045bc | ||
|
|
1b8aeee794 | ||
|
|
95f44e7106 | ||
|
|
bfaa63bb6e | ||
|
|
cefb2fefc8 | ||
|
|
df555c018b | ||
|
|
73ca5c8b0a | ||
|
|
685fb64d4b | ||
|
|
98054985d2 | ||
|
|
aabd1626a8 | ||
|
|
a720154e90 | ||
|
|
09e4cb3fc1 | ||
|
|
9fb2588100 | ||
|
|
7597b39905 | ||
|
|
79d81a6f66 | ||
|
|
9ab90ff2d8 | ||
|
|
a9490577f3 | ||
|
|
6dceafa0c4 | ||
|
|
397e699acb | ||
|
|
f2fd1bb78e | ||
|
|
0fefed234b | ||
|
|
d2cf70e09d | ||
|
|
dd4b9a2a90 | ||
|
|
c67c79ccef | ||
|
|
4c63c9c37c | ||
|
|
021ef87203 | ||
|
|
b5caf6abe4 | ||
|
|
4c003d70eb | ||
|
|
437ea448cc | ||
|
|
37a4d64ae6 | ||
|
|
efb1c5eaed | ||
|
|
724f3baf2b | ||
|
|
f6a59c5af5 | ||
|
|
9bde26153a | ||
|
|
a14e4cf4ca | ||
|
|
98d8a473f2 | ||
|
|
019c77b0aa | ||
|
|
3c009596b5 | ||
|
|
b8283ed143 | ||
|
|
00d64b136c |
22
CHANGELOG.md
22
CHANGELOG.md
@ -1,13 +1,19 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [1.1.0] - 2024-03-21
|
## [1.1.0] - 2024-01-27
|
||||||
|
|
||||||
### Modificado
|
### Adicionado
|
||||||
- Melhorado o processo de upload de áudio para evitar colisões de arquivos e garantir integridade dos dados
|
- Função de processamento de áudio (process-audio)
|
||||||
- Implementado processamento assíncrono de áudio via Edge Function
|
- Integração com OpenAI Whisper para transcrição
|
||||||
|
- Análise de leitura usando GPT-4
|
||||||
|
- Database Trigger para processamento automático de gravações
|
||||||
|
|
||||||
### Técnico
|
### Técnico
|
||||||
- Adicionado UUID para identificação única de arquivos de áudio
|
- Implementação de Edge Function no Supabase
|
||||||
- Implementada transação atômica para upload de áudio
|
- Configuração de ambiente de desenvolvimento com Docker
|
||||||
- Integrada chamada assíncrona para processamento de áudio
|
- Integração com banco de dados para story_recordings
|
||||||
- Melhorado tratamento de erros no processo de upload
|
- Sistema de análise de métricas de leitura
|
||||||
|
|
||||||
|
### Modificado
|
||||||
|
- Estrutura de armazenamento de gravações
|
||||||
|
- Fluxo de processamento de áudio automatizado
|
||||||
24
package-lock.json
generated
24
package-lock.json
generated
@ -24,14 +24,12 @@
|
|||||||
"react-router-dom": "^6.28.0",
|
"react-router-dom": "^6.28.0",
|
||||||
"recharts": "^2.15.0",
|
"recharts": "^2.15.0",
|
||||||
"resend": "^3.2.0",
|
"resend": "^3.2.0",
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5"
|
||||||
"uuid": "^11.0.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.9.1",
|
"@eslint/js": "^9.9.1",
|
||||||
"@types/react": "^18.3.17",
|
"@types/react": "^18.3.17",
|
||||||
"@types/react-dom": "^18.3.5",
|
"@types/react-dom": "^18.3.5",
|
||||||
"@types/uuid": "^10.0.0",
|
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^9.9.1",
|
"eslint": "^9.9.1",
|
||||||
@ -2475,13 +2473,6 @@
|
|||||||
"source-map": "^0.6.1"
|
"source-map": "^0.6.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/uuid": {
|
|
||||||
"version": "10.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
|
||||||
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/webpack": {
|
"node_modules/@types/webpack": {
|
||||||
"version": "4.41.40",
|
"version": "4.41.40",
|
||||||
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.40.tgz",
|
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.40.tgz",
|
||||||
@ -6365,19 +6356,6 @@
|
|||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/uuid": {
|
|
||||||
"version": "11.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz",
|
|
||||||
"integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==",
|
|
||||||
"funding": [
|
|
||||||
"https://github.com/sponsors/broofa",
|
|
||||||
"https://github.com/sponsors/ctavan"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"uuid": "dist/esm/bin/uuid"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/victory-vendor": {
|
"node_modules/victory-vendor": {
|
||||||
"version": "36.9.2",
|
"version": "36.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
|
||||||
|
|||||||
@ -31,14 +31,12 @@
|
|||||||
"react-router-dom": "^6.28.0",
|
"react-router-dom": "^6.28.0",
|
||||||
"recharts": "^2.15.0",
|
"recharts": "^2.15.0",
|
||||||
"resend": "^3.2.0",
|
"resend": "^3.2.0",
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5"
|
||||||
"uuid": "^11.0.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.9.1",
|
"@eslint/js": "^9.9.1",
|
||||||
"@types/react": "^18.3.17",
|
"@types/react": "^18.3.17",
|
||||||
"@types/react-dom": "^18.3.5",
|
"@types/react-dom": "^18.3.5",
|
||||||
"@types/uuid": "^10.0.0",
|
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^9.9.1",
|
"eslint": "^9.9.1",
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import { Mic, Square, Loader, Play, Upload } from 'lucide-react';
|
import { Mic, Square, Loader, Play, Upload } from 'lucide-react';
|
||||||
import { supabase } from '../../lib/supabase';
|
import { supabase } from '../../lib/supabase';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
interface AudioRecorderProps {
|
interface AudioRecorderProps {
|
||||||
storyId: string;
|
storyId: string;
|
||||||
@ -52,29 +51,6 @@ export function AudioRecorder({ storyId, studentId, onAudioUploaded }: AudioReco
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const triggerAudioProcessing = async (recordingData: {
|
|
||||||
id: string;
|
|
||||||
story_id: string;
|
|
||||||
student_id: string;
|
|
||||||
audio_url: string;
|
|
||||||
status: string;
|
|
||||||
}): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const { error } = await supabase.functions.invoke('process-audio', {
|
|
||||||
body: {
|
|
||||||
record: recordingData
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Erro ao iniciar processamento:', error);
|
|
||||||
// Não vamos tratar o erro aqui pois o processamento é assíncrono
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Erro ao chamar função de processamento:', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const uploadAudio = async () => {
|
const uploadAudio = async () => {
|
||||||
if (!audioBlob) return;
|
if (!audioBlob) return;
|
||||||
|
|
||||||
@ -87,75 +63,52 @@ export function AudioRecorder({ storyId, studentId, onAudioUploaded }: AudioReco
|
|||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
// Gerar um UUID único para o arquivo
|
|
||||||
const fileId = uuidv4();
|
|
||||||
const filePath = `${studentId}/${storyId}/${fileId}.webm`;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Iniciar uma transação
|
// 1. Primeiro criar o registro no banco
|
||||||
const { data: recordData, error: recordError } = await supabase
|
const { data: recordData, error: recordError } = await supabase
|
||||||
.from('story_recordings')
|
.from('story_recordings')
|
||||||
.insert({
|
.insert({
|
||||||
id: fileId, // Usar o mesmo UUID como ID do registro
|
|
||||||
story_id: storyId,
|
story_id: storyId,
|
||||||
student_id: studentId,
|
student_id: studentId,
|
||||||
status: 'uploading', // Status inicial
|
status: 'pending_analysis',
|
||||||
created_at: new Date().toISOString()
|
created_at: new Date().toISOString()
|
||||||
})
|
})
|
||||||
.select('id')
|
.select()
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (recordError) throw recordError;
|
if (recordError) throw recordError;
|
||||||
|
if (!recordData) throw new Error('Registro não criado');
|
||||||
|
|
||||||
// Upload do arquivo
|
// 2. Upload do arquivo usando o ID do registro
|
||||||
|
const filePath = `${studentId}/${storyId}/${recordData.id}.webm`;
|
||||||
|
|
||||||
const { error: uploadError } = await supabase.storage
|
const { error: uploadError } = await supabase.storage
|
||||||
.from('recordings')
|
.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'
|
||||||
upsert: false
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (uploadError) {
|
if (uploadError) {
|
||||||
// Se o upload falhar, remover o registro do banco
|
// Limpar registro se upload falhar
|
||||||
await supabase
|
await supabase.from('story_recordings').delete().eq('id', recordData.id);
|
||||||
.from('story_recordings')
|
|
||||||
.delete()
|
|
||||||
.eq('id', fileId);
|
|
||||||
throw uploadError;
|
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);
|
||||||
|
|
||||||
// Atualizar o registro com a URL e status
|
// 4. Atualizar registro com URL
|
||||||
const { error: updateError } = await supabase
|
const { error: updateError } = await supabase
|
||||||
.from('story_recordings')
|
.from('story_recordings')
|
||||||
.update({
|
.update({
|
||||||
audio_url: publicUrl,
|
audio_url: publicUrl
|
||||||
status: 'pending_analysis'
|
|
||||||
})
|
})
|
||||||
.eq('id', fileId);
|
.eq('id', recordData.id);
|
||||||
|
|
||||||
if (updateError) {
|
if (updateError) throw updateError;
|
||||||
// Se a atualização falhar, limpar tudo
|
|
||||||
await Promise.all([
|
|
||||||
supabase.storage.from('recordings').remove([filePath]),
|
|
||||||
supabase.from('story_recordings').delete().eq('id', fileId)
|
|
||||||
]);
|
|
||||||
throw updateError;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disparar o processamento de forma assíncrona
|
|
||||||
triggerAudioProcessing({
|
|
||||||
id: fileId,
|
|
||||||
story_id: storyId,
|
|
||||||
student_id: studentId,
|
|
||||||
audio_url: publicUrl,
|
|
||||||
status: 'pending_analysis'
|
|
||||||
}).catch(console.error); // Capturar erros mas não esperar pela conclusão
|
|
||||||
|
|
||||||
onAudioUploaded(publicUrl);
|
onAudioUploaded(publicUrl);
|
||||||
setAudioBlob(null);
|
setAudioBlob(null);
|
||||||
|
|||||||
@ -1,49 +0,0 @@
|
|||||||
-- Criar a trigger function
|
|
||||||
create function handle_new_recording()
|
|
||||||
returns trigger
|
|
||||||
language plpgsql
|
|
||||||
security definer
|
|
||||||
as $$
|
|
||||||
declare
|
|
||||||
response json;
|
|
||||||
begin
|
|
||||||
-- Apenas processa registros com status pending_analysis
|
|
||||||
if NEW.status = 'pending_analysis' then
|
|
||||||
-- Chama a Edge Function
|
|
||||||
select
|
|
||||||
content into response
|
|
||||||
from
|
|
||||||
http((
|
|
||||||
'POST',
|
|
||||||
current_setting('app.settings.edge_function_url') || '/process-audio',
|
|
||||||
ARRAY[
|
|
||||||
('Authorization', 'Bearer ' || current_setting('app.settings.service_role_key'))::http_header,
|
|
||||||
('Content-Type', 'application/json')::http_header
|
|
||||||
],
|
|
||||||
'application/json',
|
|
||||||
json_build_object(
|
|
||||||
'record', json_build_object(
|
|
||||||
'id', NEW.id,
|
|
||||||
'story_id', NEW.story_id,
|
|
||||||
'student_id', NEW.student_id,
|
|
||||||
'audio_url', NEW.audio_url,
|
|
||||||
'status', NEW.status
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)::http_request);
|
|
||||||
end if;
|
|
||||||
|
|
||||||
return NEW;
|
|
||||||
end;
|
|
||||||
$$;
|
|
||||||
|
|
||||||
-- Configurar as variáveis de ambiente
|
|
||||||
select set_config('app.settings.edge_function_url', 'https://bsjlbnyslxzsdwxvkaap.supabase.co/functions/v1', false);
|
|
||||||
select set_config('app.settings.service_role_key', 'seu_service_role_key', false);
|
|
||||||
|
|
||||||
-- Criar a trigger
|
|
||||||
create trigger process_new_recording
|
|
||||||
after insert or update
|
|
||||||
on story_recordings
|
|
||||||
for each row
|
|
||||||
execute function handle_new_recording();
|
|
||||||
Loading…
Reference in New Issue
Block a user