mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-16 13:27:52 +00:00
feat: adiciona integração com edge function para processamento de áudio
- Cria serviço audioService para upload e processamento - Implementa componente AudioUploader com feedback visual - Adiciona componente Button reutilizável - Integra processamento de áudio na página de histórias
This commit is contained in:
parent
6f8e890e86
commit
797967ca5b
5
.vscode/extensions.json
vendored
Normal file
5
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"denoland.vscode-deno"
|
||||
]
|
||||
}
|
||||
79
n8n.js
Normal file
79
n8n.js
Normal file
@ -0,0 +1,79 @@
|
||||
// Estrutura do fluxo
|
||||
[
|
||||
{
|
||||
// 1. Webhook Trigger
|
||||
"name": "Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"parameters": {
|
||||
"path": "audio-processing",
|
||||
"responseMode": "lastNode"
|
||||
}
|
||||
},
|
||||
{
|
||||
// 2. Download do Áudio do Supabase
|
||||
"name": "Supabase",
|
||||
"type": "n8n-nodes-base.supabase",
|
||||
"parameters": {
|
||||
"operation": "download",
|
||||
"bucket": "audios",
|
||||
"filePath": "={{$json.file_path}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
// 3. Pré-processamento do Áudio (usando FFmpeg)
|
||||
"name": "FFmpeg",
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"parameters": {
|
||||
"command": "ffmpeg -i input.wav -af 'anlmdn,highpass=f=200,lowpass=f=3000,silenceremove=1:0:-50dB' output.wav"
|
||||
}
|
||||
},
|
||||
{
|
||||
// 4. Transcrição (usando OpenAI Whisper)
|
||||
"name": "Whisper",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"parameters": {
|
||||
"url": "https://api.openai.com/v1/audio/transcriptions",
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{$env.OPENAI_API_KEY}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
// 5. Análise do Texto (usando GPT-4)
|
||||
"name": "GPT4Analysis",
|
||||
"type": "n8n-nodes-base.openAi",
|
||||
"parameters": {
|
||||
"model": "gpt-4",
|
||||
"prompt": `Analise a seguinte transcrição considerando:
|
||||
1. Fluência (velocidade, pausas, prosódia)
|
||||
2. Pronúncia (precisão fonética, clareza)
|
||||
3. Erros (substituições, omissões)
|
||||
4. Compreensão (coerência, autocorreção)
|
||||
|
||||
Transcrição: {{$node.Whisper.data.text}}
|
||||
|
||||
Forneça uma análise detalhada seguindo as métricas especificadas.`
|
||||
}
|
||||
},
|
||||
{
|
||||
// 6. Salvar Resultados no Supabase
|
||||
"name": "SaveResults",
|
||||
"type": "n8n-nodes-base.supabase",
|
||||
"parameters": {
|
||||
"operation": "insert",
|
||||
"table": "audio_analysis",
|
||||
"data": {
|
||||
"audio_path": "={{$json.file_path}}",
|
||||
"transcription": "={{$node.Whisper.data.text}}",
|
||||
"analysis": "={{$node.GPT4Analysis.data.choices[0].text}}",
|
||||
"metrics": {
|
||||
"fluency": "={{$node.GPT4Analysis.data.metrics.fluency}}",
|
||||
"pronunciation": "={{$node.GPT4Analysis.data.metrics.pronunciation}}",
|
||||
"errors": "={{$node.GPT4Analysis.data.metrics.errors}}",
|
||||
"comprehension": "={{$node.GPT4Analysis.data.metrics.comprehension}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
309
package-lock.json
generated
309
package-lock.json
generated
@ -29,6 +29,7 @@
|
||||
"eslint-plugin-react-refresh": "^0.4.11",
|
||||
"globals": "^15.9.0",
|
||||
"postcss": "^8.4.49",
|
||||
"supabase": "^2.1.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript-eslint": "^8.3.0",
|
||||
@ -907,6 +908,19 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/fs-minipass": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
||||
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"minipass": "^7.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
|
||||
@ -1979,6 +1993,16 @@
|
||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
|
||||
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
@ -2092,6 +2116,23 @@
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
},
|
||||
"node_modules/bin-links": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bin-links/-/bin-links-5.0.0.tgz",
|
||||
"integrity": "sha512-sdleLVfCjBtgO5cNjA2HVRvWBJAHs4zwenaCPMNJAJU0yNxpzj80IpjOIimkpkr+mhlA+how5poQtt53PygbHA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cmd-shim": "^7.0.0",
|
||||
"npm-normalize-package-bin": "^4.0.0",
|
||||
"proc-log": "^5.0.0",
|
||||
"read-cmd-shim": "^5.0.0",
|
||||
"write-file-atomic": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
@ -2246,6 +2287,16 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
||||
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
@ -2255,6 +2306,16 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/cmd-shim": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-7.0.0.tgz",
|
||||
"integrity": "sha512-rtpaCbr164TPPh+zFdkWpCyZuKkjpAzODfaZCf/SVJZzJN+4bHQb/LP3Jzq5/+84um3XXY8r548XiWKSborwVw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
@ -2332,6 +2393,16 @@
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
@ -2883,6 +2954,30 @@
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fetch-blob": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-domexception": "^1.0.0",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20 || >= 14.13"
|
||||
}
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
@ -2957,6 +3052,19 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fetch-blob": "^3.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fraction.js": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
||||
@ -3129,6 +3237,20 @@
|
||||
"entities": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@ -3493,6 +3615,36 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz",
|
||||
"integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minipass": "^7.0.4",
|
||||
"rimraf": "^5.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
|
||||
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mkdirp": "dist/cjs/src/bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@ -3534,6 +3686,45 @@
|
||||
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
||||
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"fetch-blob": "^3.1.4",
|
||||
"formdata-polyfill": "^4.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.18",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
|
||||
@ -3573,6 +3764,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/npm-normalize-package-bin": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz",
|
||||
"integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@ -3910,6 +4111,16 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proc-log": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz",
|
||||
"integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proto-list": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
|
||||
@ -4035,6 +4246,16 @@
|
||||
"pify": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cmd-shim": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-5.0.0.tgz",
|
||||
"integrity": "sha512-SEbJV7tohp3DAAILbEMPXavBjAnMN0tVnh4+9G8ihV4Pq3HYF9h8QNez9zkJ1ILkv9G2BjdzwctznGZXgu/HGw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
@ -4095,6 +4316,22 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "5.0.10",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
|
||||
"integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"glob": "^10.3.7"
|
||||
},
|
||||
"bin": {
|
||||
"rimraf": "dist/esm/bin.mjs"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz",
|
||||
@ -4343,6 +4580,26 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/supabase": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supabase/-/supabase-2.1.1.tgz",
|
||||
"integrity": "sha512-KPP4LrvMmu6IWIcqSOcPbepTGT2Fv05TMiYwbcEaKgCVUznDNtlyXQTImJhjP+8FhNuJ+tF9SLoQgMQqDei+jA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bin-links": "^5.0.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"node-fetch": "^3.3.2",
|
||||
"tar": "7.4.3"
|
||||
},
|
||||
"bin": {
|
||||
"supabase": "bin/supabase"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
@ -4415,6 +4672,34 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
|
||||
"integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@isaacs/fs-minipass": "^4.0.0",
|
||||
"chownr": "^3.0.0",
|
||||
"minipass": "^7.1.2",
|
||||
"minizlib": "^3.0.1",
|
||||
"mkdirp": "^3.0.1",
|
||||
"yallist": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tar/node_modules/yallist": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
@ -4645,6 +4930,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
@ -4795,6 +5090,20 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/write-file-atomic": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-6.0.0.tgz",
|
||||
"integrity": "sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"imurmurhash": "^0.1.4",
|
||||
"signal-exit": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||
|
||||
@ -33,6 +33,7 @@
|
||||
"eslint-plugin-react-refresh": "^0.4.11",
|
||||
"globals": "^15.9.0",
|
||||
"postcss": "^8.4.49",
|
||||
"supabase": "^2.1.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript-eslint": "^8.3.0",
|
||||
|
||||
67
src/components/audio/AudioUploader.tsx
Normal file
67
src/components/audio/AudioUploader.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import { processAudio } from '../../services/audioService';
|
||||
import { Button } from '../ui/button';
|
||||
|
||||
export function AudioUploader(): JSX.Element {
|
||||
const [isProcessing, setIsProcessing] = React.useState(false);
|
||||
const [transcription, setTranscription] = React.useState<string>();
|
||||
const [error, setError] = React.useState<string>();
|
||||
|
||||
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
setIsProcessing(true);
|
||||
setError(undefined);
|
||||
|
||||
const response = await processAudio(file);
|
||||
|
||||
if (response.error) {
|
||||
setError(response.error);
|
||||
} else {
|
||||
setTranscription(response.transcription);
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Erro ao processar áudio. Tente novamente.');
|
||||
console.error(err);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
accept="audio/*"
|
||||
onChange={handleFileUpload}
|
||||
disabled={isProcessing}
|
||||
className="hidden"
|
||||
id="audio-upload"
|
||||
/>
|
||||
<label htmlFor="audio-upload">
|
||||
<Button
|
||||
as="span"
|
||||
disabled={isProcessing}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{isProcessing ? 'Processando...' : 'Enviar Áudio'}
|
||||
</Button>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="text-red-500 text-sm">{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>
|
||||
);
|
||||
}
|
||||
47
src/components/layouts/DashboardLayout.tsx
Normal file
47
src/components/layouts/DashboardLayout.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { DashboardSidebar } from './DashboardSidebar';
|
||||
|
||||
interface DashboardLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function DashboardLayout({ children }: DashboardLayoutProps): JSX.Element {
|
||||
const [sidebarOpen, setSidebarOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||
type="button"
|
||||
className="inline-flex items-center p-2 mt-2 ms-3 text-sm text-gray-500 rounded-lg sm:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
|
||||
>
|
||||
<span className="sr-only">Abrir menu</span>
|
||||
<svg className="w-6 h-6" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path clipRule="evenodd" fillRule="evenodd" d="M2 4.75A.75.75 0 012.75 4h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 4.75zm0 10.5a.75.75 0 01.75-.75h7.5a.75.75 0 010 1.5h-7.5a.75.75 0 01-.75-.75zM2 10a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 10z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<aside
|
||||
className={`fixed top-0 left-0 z-40 w-64 h-screen transition-transform ${
|
||||
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
|
||||
} sm:translate-x-0`}
|
||||
>
|
||||
<div className="h-full px-3 py-4 overflow-y-auto bg-white border-r border-gray-200 dark:bg-gray-800 dark:border-gray-700">
|
||||
<div className="flex items-center pb-4 mb-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<img src="/logo.svg" className="h-8 me-3" alt="Logo" />
|
||||
<span className="self-center text-xl font-semibold whitespace-nowrap dark:text-white">
|
||||
Histórias Mágicas
|
||||
</span>
|
||||
</div>
|
||||
<DashboardSidebar />
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div className="p-4 sm:ml-64">
|
||||
<div className="p-4 border-2 border-gray-200 border-dashed rounded-lg dark:border-gray-700">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
39
src/components/layouts/DashboardPageLayout.tsx
Normal file
39
src/components/layouts/DashboardPageLayout.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { DashboardSidebar } from './DashboardSidebar';
|
||||
|
||||
interface DashboardPageLayoutProps {
|
||||
children: React.ReactNode;
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export function DashboardPageLayout({
|
||||
children,
|
||||
title,
|
||||
description
|
||||
}: DashboardPageLayoutProps): JSX.Element {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex">
|
||||
<DashboardSidebar />
|
||||
|
||||
<main className="flex-1 min-h-screen transition-all duration-300">
|
||||
<div className="w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<div className="mb-6 sm:mb-8">
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-primary mb-2">
|
||||
{title}
|
||||
</h1>
|
||||
{description && (
|
||||
<p className="text-sm sm:text-base text-gray-600">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
37
src/components/layouts/DashboardSidebar.tsx
Normal file
37
src/components/layouts/DashboardSidebar.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { LayoutDashboard, Users, GraduationCap, UserCircle, BookOpen, Settings, LogOut } from 'lucide-react';
|
||||
|
||||
const links = [
|
||||
{ icon: <LayoutDashboard className="w-5 h-5" />, label: 'Visão Geral', href: '/dashboard' },
|
||||
{ icon: <Users className="w-5 h-5" />, label: 'Turmas', href: '/dashboard/turmas' },
|
||||
{ icon: <GraduationCap className="w-5 h-5" />, label: 'Professores', href: '/dashboard/professores' },
|
||||
{ icon: <UserCircle className="w-5 h-5" />, label: 'Alunos', href: '/dashboard/alunos' },
|
||||
{ icon: <BookOpen className="w-5 h-5" />, label: 'Histórias', href: '/dashboard/historias' },
|
||||
{ icon: <Settings className="w-5 h-5" />, label: 'Configurações', href: '/dashboard/configuracoes' },
|
||||
{ icon: <LogOut className="w-5 h-5" />, label: 'Sair', href: '/logout' },
|
||||
];
|
||||
|
||||
export function DashboardSidebar(): JSX.Element {
|
||||
return (
|
||||
<ul className="space-y-2 font-medium">
|
||||
{links.map((link) => (
|
||||
<li key={link.href}>
|
||||
<NavLink
|
||||
to={link.href}
|
||||
className={({ isActive }) => `
|
||||
flex items-center p-2 text-gray-900 rounded-lg dark:text-white
|
||||
hover:bg-gray-100 dark:hover:bg-gray-700 group
|
||||
${isActive ? 'bg-gray-100 dark:bg-gray-700' : ''}
|
||||
`}
|
||||
>
|
||||
<div className="flex-shrink-0 w-5 h-5 text-gray-500 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white">
|
||||
{link.icon}
|
||||
</div>
|
||||
<span className="flex-1 ms-3 whitespace-nowrap">{link.label}</span>
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
30
src/components/ui/button.tsx
Normal file
30
src/components/ui/button.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
as?: 'button' | 'span';
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Button({
|
||||
as: Component = 'button',
|
||||
className = '',
|
||||
children,
|
||||
...props
|
||||
}: ButtonProps): JSX.Element {
|
||||
return (
|
||||
<Component
|
||||
className={`
|
||||
inline-flex items-center justify-center px-4 py-2
|
||||
text-sm font-medium text-white
|
||||
bg-purple-600 hover:bg-purple-700
|
||||
rounded-md shadow-sm
|
||||
transition-colors duration-200
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
10
src/pages/dashboard/DashboardPage.tsx
Normal file
10
src/pages/dashboard/DashboardPage.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { DashboardLayout } from "../../components/layouts/DashboardLayout";
|
||||
import React from "react";
|
||||
|
||||
export function DashboardPage(): JSX.Element {
|
||||
return (
|
||||
<DashboardLayout>
|
||||
{/* Conteúdo da página */}
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
38
src/services/audioService.ts
Normal file
38
src/services/audioService.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { supabase } from '../lib/supabase';
|
||||
|
||||
interface ProcessAudioResponse {
|
||||
transcription?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function processAudio(audioFile: File): Promise<ProcessAudioResponse> {
|
||||
try {
|
||||
// 1. Upload do arquivo para o bucket do Supabase
|
||||
const fileName = `audio-${Date.now()}-${audioFile.name}`;
|
||||
const { data: uploadData, error: uploadError } = await supabase.storage
|
||||
.from('audio-uploads')
|
||||
.upload(fileName, audioFile);
|
||||
|
||||
if (uploadError) throw new uploadError;
|
||||
|
||||
// 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 (error) throw error;
|
||||
|
||||
return {
|
||||
transcription: data?.transcription
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erro ao processar áudio:', error);
|
||||
return {
|
||||
error: 'Falha ao processar o áudio. Tente novamente.'
|
||||
};
|
||||
}
|
||||
}
|
||||
4
supabase/.gitignore
vendored
Normal file
4
supabase/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
# Supabase
|
||||
.branches
|
||||
.temp
|
||||
.env
|
||||
22
supabase/config.toml
Normal file
22
supabase/config.toml
Normal file
@ -0,0 +1,22 @@
|
||||
project_id = "bsjlbnyslxzsdwxvkaap"
|
||||
|
||||
[auth]
|
||||
enabled = true
|
||||
site_url = "https://historiasmagicas.netlify.app"
|
||||
additional_redirect_urls = ["https://historiasmagicas.netlify.app", "https://*.historiasmagicas.netlify.app"]
|
||||
jwt_expiry = 3600
|
||||
enable_refresh_token_rotation = true
|
||||
refresh_token_reuse_interval = 10
|
||||
|
||||
[auth.mfa.totp]
|
||||
enroll_enabled = true
|
||||
verify_enabled = true
|
||||
|
||||
[auth.email]
|
||||
enable_signup = true
|
||||
double_confirm_changes = true
|
||||
enable_confirmations = true
|
||||
secure_password_change = false
|
||||
max_frequency = "1m0s"
|
||||
otp_length = 6
|
||||
otp_expiry = 86400
|
||||
127
supabase/functions/process-audio/index.ts
Normal file
127
supabase/functions/process-audio/index.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
|
||||
import { Configuration, OpenAIApi } from 'https://esm.sh/openai@3.1.0'
|
||||
|
||||
// Configurar OpenAI
|
||||
const openaiConfig = new Configuration({
|
||||
apiKey: Deno.env.get('OPENAI_API_KEY')
|
||||
})
|
||||
const openai = new OpenAIApi(openaiConfig)
|
||||
|
||||
// Configurar Supabase
|
||||
const supabaseUrl = Deno.env.get('SUPABASE_URL')
|
||||
const supabaseAnonKey = Deno.env.get('SUPABASE_ANON_KEY')
|
||||
const supabase = createClient(supabaseUrl, supabaseAnonKey)
|
||||
|
||||
serve(async (req) => {
|
||||
try {
|
||||
// Extrair dados do webhook
|
||||
const { record } = await req.json()
|
||||
const { id, audio_url, story_id } = record
|
||||
|
||||
// Atualizar status para processing
|
||||
await supabase
|
||||
.from('story_recordings')
|
||||
.update({ status: 'processing' })
|
||||
.eq('id', id)
|
||||
|
||||
// Buscar texto original da história
|
||||
const { data: storyData } = await supabase
|
||||
.from('stories')
|
||||
.select('content')
|
||||
.eq('id', story_id)
|
||||
.single()
|
||||
|
||||
const originalText = storyData.content.pages[0].text
|
||||
|
||||
// 1. Transcrever áudio com Whisper
|
||||
const audioResponse = await fetch(audio_url)
|
||||
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')
|
||||
.update({
|
||||
transcription: transcription.data.text,
|
||||
metrics: analysisResult.metrics,
|
||||
feedback: analysisResult.feedback,
|
||||
details: analysisResult.details,
|
||||
status: 'analyzed',
|
||||
processed_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', id)
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: true }),
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erro:', error)
|
||||
|
||||
// Atualizar registro com erro
|
||||
if (error.record?.id) {
|
||||
await supabase
|
||||
.from('story_recordings')
|
||||
.update({
|
||||
status: 'error',
|
||||
error_message: error.message
|
||||
})
|
||||
.eq('id', error.record.id)
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ error: error.message }),
|
||||
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
}
|
||||
})
|
||||
0
supabase/seed.sql
Normal file
0
supabase/seed.sql
Normal file
Loading…
Reference in New Issue
Block a user