refactor: streamline voice module and remove unused code
- Removed the `file` property from the `Speak` type in `speak.ts`. - Simplified exports in `index.ts` by removing unused functions. - Deleted the `relatime.ts` file as it was no longer needed. - Cleaned up `settingStore.ts` by removing Volcengine configuration properties and related methods. - Updated `voiceStore.ts` to use Flowme API for voice management and removed unnecessary methods. - Changed the main export in `page.tsx` to point to the new App component. - Introduced new `asr-api.ts` and `flowme-api.ts` modules for handling API interactions. - Added `ConfirmDeleteModal.tsx` for confirming voice deletions. - Created `resourceStore.ts` for managing resource uploads.
This commit is contained in:
@@ -18,7 +18,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@base-ui/react": "^1.2.0",
|
"@base-ui/react": "^1.2.0",
|
||||||
"@kevisual/api": "^0.0.62",
|
"@kevisual/api": "^0.0.63",
|
||||||
"@kevisual/context": "^0.0.8",
|
"@kevisual/context": "^0.0.8",
|
||||||
"@kevisual/router": "0.1.1",
|
"@kevisual/router": "0.1.1",
|
||||||
"@kevisual/video-tools": "^0.0.13",
|
"@kevisual/video-tools": "^0.0.13",
|
||||||
@@ -69,6 +69,6 @@
|
|||||||
"tailwindcss": "^4.2.1",
|
"tailwindcss": "^4.2.1",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "v8.0.0-beta.16"
|
"vite": "v8.0.0-beta.18"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
193
pnpm-lock.yaml
generated
193
pnpm-lock.yaml
generated
@@ -12,8 +12,8 @@ importers:
|
|||||||
specifier: ^1.2.0
|
specifier: ^1.2.0
|
||||||
version: 1.2.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 1.2.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
'@kevisual/api':
|
'@kevisual/api':
|
||||||
specifier: ^0.0.62
|
specifier: ^0.0.63
|
||||||
version: 0.0.62(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
|
version: 0.0.63(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
|
||||||
'@kevisual/context':
|
'@kevisual/context':
|
||||||
specifier: ^0.0.8
|
specifier: ^0.0.8
|
||||||
version: 0.0.8
|
version: 0.0.8
|
||||||
@@ -113,16 +113,16 @@ importers:
|
|||||||
version: 0.0.12
|
version: 0.0.12
|
||||||
'@kevisual/vite-html-plugin':
|
'@kevisual/vite-html-plugin':
|
||||||
specifier: ^0.0.1
|
specifier: ^0.0.1
|
||||||
version: 0.0.1(vite@8.0.0-beta.16(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))
|
version: 0.0.1(vite@8.0.0-beta.18(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))
|
||||||
'@tailwindcss/vite':
|
'@tailwindcss/vite':
|
||||||
specifier: ^4.2.1
|
specifier: ^4.2.1
|
||||||
version: 4.2.1(vite@8.0.0-beta.16(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))
|
version: 4.2.1(vite@8.0.0-beta.18(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))
|
||||||
'@tanstack/react-router-devtools':
|
'@tanstack/react-router-devtools':
|
||||||
specifier: ^1.166.7
|
specifier: ^1.166.7
|
||||||
version: 1.166.7(@tanstack/react-router@1.166.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.166.7)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 1.166.7(@tanstack/react-router@1.166.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.166.7)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
'@tanstack/router-plugin':
|
'@tanstack/router-plugin':
|
||||||
specifier: ^1.166.7
|
specifier: ^1.166.7
|
||||||
version: 1.166.7(@tanstack/react-router@1.166.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0-beta.16(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))
|
version: 1.166.7(@tanstack/react-router@1.166.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0-beta.18(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^25.4.0
|
specifier: ^25.4.0
|
||||||
version: 25.4.0
|
version: 25.4.0
|
||||||
@@ -134,7 +134,7 @@ importers:
|
|||||||
version: 19.2.3(@types/react@19.2.14)
|
version: 19.2.3(@types/react@19.2.14)
|
||||||
'@vitejs/plugin-react':
|
'@vitejs/plugin-react':
|
||||||
specifier: ^5.1.4
|
specifier: ^5.1.4
|
||||||
version: 5.1.4(vite@8.0.0-beta.16(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))
|
version: 5.1.4(vite@8.0.0-beta.18(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^17.3.1
|
specifier: ^17.3.1
|
||||||
version: 17.3.1
|
version: 17.3.1
|
||||||
@@ -151,8 +151,8 @@ importers:
|
|||||||
specifier: ^5.9.3
|
specifier: ^5.9.3
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vite:
|
vite:
|
||||||
specifier: v8.0.0-beta.16
|
specifier: v8.0.0-beta.18
|
||||||
version: 8.0.0-beta.16(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)
|
version: 8.0.0-beta.18(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -672,15 +672,12 @@ packages:
|
|||||||
'@kevisual/ai@0.0.28':
|
'@kevisual/ai@0.0.28':
|
||||||
resolution: {integrity: sha512-GLwCNXfopDvOj+hEZwEIwOV2/3VGd+TCPgBClaYuAv30KzhgehlCW05HPjBducSg+uPcdKacEzZsecHjo5fMUQ==}
|
resolution: {integrity: sha512-GLwCNXfopDvOj+hEZwEIwOV2/3VGd+TCPgBClaYuAv30KzhgehlCW05HPjBducSg+uPcdKacEzZsecHjo5fMUQ==}
|
||||||
|
|
||||||
'@kevisual/api@0.0.62':
|
'@kevisual/api@0.0.63':
|
||||||
resolution: {integrity: sha512-GB8Ho2absXoXoZP2GKyuoRqRqjdwtV0JR512DXBaKJR2sIPn1KvuglbBiX+zPjDBBskv/ApvZKOoSwj1OmkrKQ==}
|
resolution: {integrity: sha512-juED4uDgHE9t6kfQRoktn6D1+LoA2bUBaoihTotVP1Cx2b14hBLZEbv0f/iNFzKfRwyYCwwTW6hRjjumOaFibQ==}
|
||||||
|
|
||||||
'@kevisual/context@0.0.8':
|
'@kevisual/context@0.0.8':
|
||||||
resolution: {integrity: sha512-DTJpyHI34NE76B7g6f+QlIqiCCyqI2qkBMQE736dzeRDGxOjnbe2iQY9W+Rt2PE6kmymM3qyOmSfNovyWyWrkA==}
|
resolution: {integrity: sha512-DTJpyHI34NE76B7g6f+QlIqiCCyqI2qkBMQE736dzeRDGxOjnbe2iQY9W+Rt2PE6kmymM3qyOmSfNovyWyWrkA==}
|
||||||
|
|
||||||
'@kevisual/js-filter@0.0.5':
|
|
||||||
resolution: {integrity: sha512-+S+Sf3K/aP6XtZI2s7TgKOr35UuvUvtpJ9YDW30a+mY0/N8gRuzyKhieBzQN7Ykayzz70uoMavBXut2rUlLgzw==}
|
|
||||||
|
|
||||||
'@kevisual/js-filter@0.0.6':
|
'@kevisual/js-filter@0.0.6':
|
||||||
resolution: {integrity: sha512-FcbOsmS1inhwrfgXMM/XLFTGTHUxBCss32JEMYdEFWQDYCar5rN8cxD1W8FuKDTVRlpA+zBpQ/BE6XT4UaeljA==}
|
resolution: {integrity: sha512-FcbOsmS1inhwrfgXMM/XLFTGTHUxBCss32JEMYdEFWQDYCar5rN8cxD1W8FuKDTVRlpA+zBpQ/BE6XT4UaeljA==}
|
||||||
|
|
||||||
@@ -1016,83 +1013,97 @@ packages:
|
|||||||
'@ricky0123/vad-web@0.0.30':
|
'@ricky0123/vad-web@0.0.30':
|
||||||
resolution: {integrity: sha512-cJyYrh4YeeUBJcbR9Bic/bFDyB9qBkAepvpuWM3vLxnAi7bC3VHzf51UeNdT+OtY4D7MLAgV8iJMc4z41ZnaWg==}
|
resolution: {integrity: sha512-cJyYrh4YeeUBJcbR9Bic/bFDyB9qBkAepvpuWM3vLxnAi7bC3VHzf51UeNdT+OtY4D7MLAgV8iJMc4z41ZnaWg==}
|
||||||
|
|
||||||
'@rolldown/binding-android-arm64@1.0.0-rc.6':
|
'@rolldown/binding-android-arm64@1.0.0-rc.8':
|
||||||
resolution: {integrity: sha512-kvjTSWGcrv+BaR2vge57rsKiYdVR8V8CoS0vgKrc570qRBfty4bT+1X0z3j2TaVV+kAYzA0PjeB9+mdZyqUZlg==}
|
resolution: {integrity: sha512-5bcmMQDWEfWUq3m79Mcf/kbO6e5Jr6YjKSsA1RnpXR6k73hQ9z1B17+4h93jXpzHvS18p7bQHM1HN/fSd+9zog==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [android]
|
os: [android]
|
||||||
|
|
||||||
'@rolldown/binding-darwin-arm64@1.0.0-rc.6':
|
'@rolldown/binding-darwin-arm64@1.0.0-rc.8':
|
||||||
resolution: {integrity: sha512-+tJhD21KvGNtUrpLXrZQlT+j5HZKiEwR2qtcZb3vNOUpvoT9QjEykr75ZW/Kr0W89gose/HVXU6351uVZD8Qvw==}
|
resolution: {integrity: sha512-dcHPd5N4g9w2iiPRJmAvO0fsIWzF2JPr9oSuTjxLL56qu+oML5aMbBMNwWbk58Mt3pc7vYs9CCScwLxdXPdRsg==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@rolldown/binding-darwin-x64@1.0.0-rc.6':
|
'@rolldown/binding-darwin-x64@1.0.0-rc.8':
|
||||||
resolution: {integrity: sha512-DKNhjMk38FAWaHwUt1dFR3rA/qRAvn2NUvSG2UGvxvlMxSmN/qqww/j4ABAbXhNRXtGQNmrAINMXRuwHl16ZHg==}
|
resolution: {integrity: sha512-mw0VzDvoj8AuR761QwpdCFN0sc/jspuc7eRYJetpLWd+XyansUrH3C7IgNw6swBOgQT9zBHNKsVCjzpfGJlhUA==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@rolldown/binding-freebsd-x64@1.0.0-rc.6':
|
'@rolldown/binding-freebsd-x64@1.0.0-rc.8':
|
||||||
resolution: {integrity: sha512-8TThsRkCPAnfyMBShxrGdtoOE6h36QepqRQI97iFaQSCRbHFWHcDHppcojZnzXoruuhPnjMEygzaykvPVJsMRg==}
|
resolution: {integrity: sha512-xNrRa6mQ9NmMIJBdJtPMPG8Mso0OhM526pDzc/EKnRrIrrkHD1E0Z6tONZRmUeJElfsQ6h44lQQCcDilSNIvSQ==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [freebsd]
|
os: [freebsd]
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.6':
|
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.8':
|
||||||
resolution: {integrity: sha512-ZfmFoOwPUZCWtGOVC9/qbQzfc0249FrRUOzV2XabSMUV60Crp211OWLQN1zmQAsRIVWRcEwhJ46Z1mXGo/L/nQ==}
|
resolution: {integrity: sha512-WgCKoO6O/rRUwimWfEJDeztwJJmuuX0N2bYLLRxmXDTtCwjToTOqk7Pashl/QpQn3H/jHjx0b5yCMbcTVYVpNg==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.6':
|
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.8':
|
||||||
resolution: {integrity: sha512-ZsGzbNETxPodGlLTYHaCSGVhNN/rvkMDCJYHdT7PZr5jFJRmBfmDi2awhF64Dt2vxrJqY6VeeYSgOzEbHRsb7Q==}
|
resolution: {integrity: sha512-tOHgTOQa8G4Z3ULj4G3NYOGGJEsqPHR91dT72u63OtVsZ7B6wFJKOx+ZKv+pvwzxWz92/I2ycaqi2/Ll4l+rlg==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
libc: [glibc]
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.6':
|
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.8':
|
||||||
resolution: {integrity: sha512-elPpdevtCdUOqziemR86C4CSCr/5sUxalzDrf/CJdMT+kZt2C556as++qHikNOz0vuFf52h+GJNXZM08eWgGPQ==}
|
resolution: {integrity: sha512-oRbxcgDujCi2Yp1GTxoUFsIFlZsuPHU4OV4AzNc3/6aUmR4lfm9FK0uwQu82PJsuUwnF2jFdop3Ep5c1uK7Uxg==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
libc: [musl]
|
||||||
|
|
||||||
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.6':
|
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.8':
|
||||||
resolution: {integrity: sha512-IBwXsf56o3xhzAyaZxdM1CX8UFiBEUFCjiVUgny67Q8vPIqkjzJj0YKhd3TbBHanuxThgBa59f6Pgutg2OGk5A==}
|
resolution: {integrity: sha512-oaLRyUHw8kQE5M89RqrDJZ10GdmGJcMeCo8tvaE4ukOofqgjV84AbqBSH6tTPjeT2BHv+xlKj678GBuIb47lKA==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.8':
|
||||||
|
resolution: {integrity: sha512-1hjSKFrod5MwBBdLOOA0zpUuSfSDkYIY+QqcMcIU1WOtswZtZdUkcFcZza9b2HcAb0bnpmmyo0LZcaxLb2ov1g==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.8':
|
||||||
|
resolution: {integrity: sha512-a1+F0aV4Wy9tT3o+cHl3XhOy6aFV+B8Ll+/JFj98oGkb6lGk3BNgrxd+80RwYRVd23oLGvj3LwluKYzlv1PEuw==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
libc: [glibc]
|
||||||
|
|
||||||
'@rolldown/binding-linux-x64-musl@1.0.0-rc.6':
|
'@rolldown/binding-linux-x64-musl@1.0.0-rc.8':
|
||||||
resolution: {integrity: sha512-vOk7G8V9Zm+8a6PL6JTpCea61q491oYlGtO6CvnsbhNLlKdf0bbCPytFzGQhYmCKZDKkEbmnkcIprTEGCURnwg==}
|
resolution: {integrity: sha512-bGyXCFU11seFrf7z8PcHSwGEiFVkZ9vs+auLacVOQrVsI8PFHJzzJROF3P6b0ODDmXr0m6Tj5FlDhcXVk0Jp8w==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
libc: [musl]
|
||||||
|
|
||||||
'@rolldown/binding-openharmony-arm64@1.0.0-rc.6':
|
'@rolldown/binding-openharmony-arm64@1.0.0-rc.8':
|
||||||
resolution: {integrity: sha512-ASjEDI4MRv7XCQb2JVaBzfEYO98JKCGrAgoW6M03fJzH/ilCnC43Mb3ptB9q/lzsaahoJyIBoAGKAYEjUvpyvQ==}
|
resolution: {integrity: sha512-n8d+L2bKgf9G3+AM0bhHFWdlz9vYKNim39ujRTieukdRek0RAo2TfG2uEnV9spa4r4oHUfL9IjcY3M9SlqN1gw==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [openharmony]
|
os: [openharmony]
|
||||||
|
|
||||||
'@rolldown/binding-wasm32-wasi@1.0.0-rc.6':
|
'@rolldown/binding-wasm32-wasi@1.0.0-rc.8':
|
||||||
resolution: {integrity: sha512-mYa1+h2l6Zc0LvmwUh0oXKKYihnw/1WC73vTqw+IgtfEtv47A+rWzzcWwVDkW73+UDr0d/Ie/HRXoaOY22pQDw==}
|
resolution: {integrity: sha512-4R4iJDIk7BrJdteAbEAICXPoA7vZoY/M0OBfcRlQxzQvUYMcEp2GbC/C8UOgQJhu2TjGTpX1H8vVO1xHWcRqQA==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
cpu: [wasm32]
|
cpu: [wasm32]
|
||||||
|
|
||||||
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.6':
|
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.8':
|
||||||
resolution: {integrity: sha512-e2ABskbNH3MRUBMjgxaMjYIw11DSwjLJxBII3UgpF6WClGLIh8A20kamc+FKH5vIaFVnYQInmcLYSUVpqMPLow==}
|
resolution: {integrity: sha512-3lwnklba9qQOpFnQ7EW+A1m4bZTWXZE4jtehsZ0YOl2ivW1FQqp5gY7X2DLuKITggesyuLwcmqS11fA7NtrmrA==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.6':
|
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.8':
|
||||||
resolution: {integrity: sha512-dJVc3ifhaRXxIEh1xowLohzFrlQXkJ66LepHm+CmSprTWgVrPa8Fx3OL57xwIqDEH9hufcKkDX2v65rS3NZyRA==}
|
resolution: {integrity: sha512-VGjCx9Ha1P/r3tXGDZyG0Fcq7Q0Afnk64aaKzr1m40vbn1FL8R3W0V1ELDvPgzLXaaqK/9PnsqSaLWXfn6JtGQ==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
@@ -1100,8 +1111,8 @@ packages:
|
|||||||
'@rolldown/pluginutils@1.0.0-rc.3':
|
'@rolldown/pluginutils@1.0.0-rc.3':
|
||||||
resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==}
|
resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==}
|
||||||
|
|
||||||
'@rolldown/pluginutils@1.0.0-rc.6':
|
'@rolldown/pluginutils@1.0.0-rc.8':
|
||||||
resolution: {integrity: sha512-Y0+JT8Mi1mmW08K6HieG315XNRu4L0rkfCpA364HtytjgiqYnMYRdFPcxRl+BQQqNXzecL2S9nii+RUpO93XIA==}
|
resolution: {integrity: sha512-wzJwL82/arVfeSP3BLr1oTy40XddjtEdrdgtJ4lLRBu06mP3q/8HGM6K0JRlQuTA3XB0pNJx2so/nmpY4xyOew==}
|
||||||
|
|
||||||
'@standard-schema/spec@1.1.0':
|
'@standard-schema/spec@1.1.0':
|
||||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||||
@@ -1940,8 +1951,8 @@ packages:
|
|||||||
resolve-pkg-maps@1.0.0:
|
resolve-pkg-maps@1.0.0:
|
||||||
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||||
|
|
||||||
rolldown@1.0.0-rc.6:
|
rolldown@1.0.0-rc.8:
|
||||||
resolution: {integrity: sha512-B8vFPV1ADyegoYfhg+E7RAucYKv0xdVlwYYsIJgfPNeiSxZGWNxts9RqhyGzC11ULK/VaeXyKezGCwpMiH8Ktw==}
|
resolution: {integrity: sha512-RGOL7mz/aoQpy/y+/XS9iePBfeNRDUdozrhCEJxdpJyimW8v6yp4c30q6OviUU5AnUJVLRL9GP//HUs6N3ALrQ==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@@ -2097,8 +2108,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
vite@8.0.0-beta.16:
|
vite@8.0.0-beta.18:
|
||||||
resolution: {integrity: sha512-c0t7hYkxsjws89HH+BUFh/sL3BpPNhNsL9CJrTpMxBmwKQBRSa5OJ5w4o9O0bQVI/H/vx7UpUUIevvXa37NS/Q==}
|
resolution: {integrity: sha512-azgNbWdsO/WBqHQxwSCy+zd+Fq+37Fix2hn64cQuiUvaaGGSUac7f8RGQhI1aQl9OKbfWblrCFLWs+tln06c2A==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2601,10 +2612,10 @@ snapshots:
|
|||||||
ai: 6.0.116(zod@4.3.6)
|
ai: 6.0.116(zod@4.3.6)
|
||||||
zod: 4.3.6
|
zod: 4.3.6
|
||||||
|
|
||||||
'@kevisual/api@0.0.62(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))':
|
'@kevisual/api@0.0.63(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@kevisual/context': 0.0.8
|
'@kevisual/context': 0.0.8
|
||||||
'@kevisual/js-filter': 0.0.5
|
'@kevisual/js-filter': 0.0.6
|
||||||
'@kevisual/load': 0.0.6
|
'@kevisual/load': 0.0.6
|
||||||
'@paralleldrive/cuid2': 3.3.0
|
'@paralleldrive/cuid2': 3.3.0
|
||||||
es-toolkit: 1.45.1
|
es-toolkit: 1.45.1
|
||||||
@@ -2624,8 +2635,6 @@ snapshots:
|
|||||||
|
|
||||||
'@kevisual/context@0.0.8': {}
|
'@kevisual/context@0.0.8': {}
|
||||||
|
|
||||||
'@kevisual/js-filter@0.0.5': {}
|
|
||||||
|
|
||||||
'@kevisual/js-filter@0.0.6': {}
|
'@kevisual/js-filter@0.0.6': {}
|
||||||
|
|
||||||
'@kevisual/kv-login@0.1.17': {}
|
'@kevisual/kv-login@0.1.17': {}
|
||||||
@@ -2680,9 +2689,9 @@ snapshots:
|
|||||||
|
|
||||||
'@kevisual/video@0.0.2': {}
|
'@kevisual/video@0.0.2': {}
|
||||||
|
|
||||||
'@kevisual/vite-html-plugin@0.0.1(vite@8.0.0-beta.16(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))':
|
'@kevisual/vite-html-plugin@0.0.1(vite@8.0.0-beta.18(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
vite: 8.0.0-beta.16(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)
|
vite: 8.0.0-beta.18(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@1.1.1':
|
'@napi-rs/wasm-runtime@1.1.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -2983,50 +2992,56 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
onnxruntime-web: 1.24.3
|
onnxruntime-web: 1.24.3
|
||||||
|
|
||||||
'@rolldown/binding-android-arm64@1.0.0-rc.6':
|
'@rolldown/binding-android-arm64@1.0.0-rc.8':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-darwin-arm64@1.0.0-rc.6':
|
'@rolldown/binding-darwin-arm64@1.0.0-rc.8':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-darwin-x64@1.0.0-rc.6':
|
'@rolldown/binding-darwin-x64@1.0.0-rc.8':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-freebsd-x64@1.0.0-rc.6':
|
'@rolldown/binding-freebsd-x64@1.0.0-rc.8':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.6':
|
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.8':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.6':
|
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.8':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.6':
|
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.8':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.6':
|
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.8':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-linux-x64-musl@1.0.0-rc.6':
|
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.8':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-openharmony-arm64@1.0.0-rc.6':
|
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.8':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-wasm32-wasi@1.0.0-rc.6':
|
'@rolldown/binding-linux-x64-musl@1.0.0-rc.8':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-openharmony-arm64@1.0.0-rc.8':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-wasm32-wasi@1.0.0-rc.8':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@napi-rs/wasm-runtime': 1.1.1
|
'@napi-rs/wasm-runtime': 1.1.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.6':
|
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.8':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.6':
|
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.8':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/pluginutils@1.0.0-rc.3': {}
|
'@rolldown/pluginutils@1.0.0-rc.3': {}
|
||||||
|
|
||||||
'@rolldown/pluginutils@1.0.0-rc.6': {}
|
'@rolldown/pluginutils@1.0.0-rc.8': {}
|
||||||
|
|
||||||
'@standard-schema/spec@1.1.0': {}
|
'@standard-schema/spec@1.1.0': {}
|
||||||
|
|
||||||
@@ -3097,12 +3112,12 @@ snapshots:
|
|||||||
'@tailwindcss/oxide-win32-arm64-msvc': 4.2.1
|
'@tailwindcss/oxide-win32-arm64-msvc': 4.2.1
|
||||||
'@tailwindcss/oxide-win32-x64-msvc': 4.2.1
|
'@tailwindcss/oxide-win32-x64-msvc': 4.2.1
|
||||||
|
|
||||||
'@tailwindcss/vite@4.2.1(vite@8.0.0-beta.16(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))':
|
'@tailwindcss/vite@4.2.1(vite@8.0.0-beta.18(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tailwindcss/node': 4.2.1
|
'@tailwindcss/node': 4.2.1
|
||||||
'@tailwindcss/oxide': 4.2.1
|
'@tailwindcss/oxide': 4.2.1
|
||||||
tailwindcss: 4.2.1
|
tailwindcss: 4.2.1
|
||||||
vite: 8.0.0-beta.16(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)
|
vite: 8.0.0-beta.18(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)
|
||||||
|
|
||||||
'@tanstack/history@1.161.4': {}
|
'@tanstack/history@1.161.4': {}
|
||||||
|
|
||||||
@@ -3174,7 +3189,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@tanstack/router-plugin@1.166.7(@tanstack/react-router@1.166.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0-beta.16(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))':
|
'@tanstack/router-plugin@1.166.7(@tanstack/react-router@1.166.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0-beta.18(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.29.0
|
||||||
'@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0)
|
'@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0)
|
||||||
@@ -3191,7 +3206,7 @@ snapshots:
|
|||||||
zod: 3.25.76
|
zod: 3.25.76
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@tanstack/react-router': 1.166.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
'@tanstack/react-router': 1.166.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
vite: 8.0.0-beta.16(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)
|
vite: 8.0.0-beta.18(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -3253,7 +3268,7 @@ snapshots:
|
|||||||
|
|
||||||
'@vercel/oidc@3.1.0': {}
|
'@vercel/oidc@3.1.0': {}
|
||||||
|
|
||||||
'@vitejs/plugin-react@5.1.4(vite@8.0.0-beta.16(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))':
|
'@vitejs/plugin-react@5.1.4(vite@8.0.0-beta.18(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.29.0
|
||||||
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0)
|
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0)
|
||||||
@@ -3261,7 +3276,7 @@ snapshots:
|
|||||||
'@rolldown/pluginutils': 1.0.0-rc.3
|
'@rolldown/pluginutils': 1.0.0-rc.3
|
||||||
'@types/babel__core': 7.20.5
|
'@types/babel__core': 7.20.5
|
||||||
react-refresh: 0.18.0
|
react-refresh: 0.18.0
|
||||||
vite: 8.0.0-beta.16(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)
|
vite: 8.0.0-beta.18(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -3826,24 +3841,26 @@ snapshots:
|
|||||||
|
|
||||||
resolve-pkg-maps@1.0.0: {}
|
resolve-pkg-maps@1.0.0: {}
|
||||||
|
|
||||||
rolldown@1.0.0-rc.6:
|
rolldown@1.0.0-rc.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@oxc-project/types': 0.115.0
|
'@oxc-project/types': 0.115.0
|
||||||
'@rolldown/pluginutils': 1.0.0-rc.6
|
'@rolldown/pluginutils': 1.0.0-rc.8
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@rolldown/binding-android-arm64': 1.0.0-rc.6
|
'@rolldown/binding-android-arm64': 1.0.0-rc.8
|
||||||
'@rolldown/binding-darwin-arm64': 1.0.0-rc.6
|
'@rolldown/binding-darwin-arm64': 1.0.0-rc.8
|
||||||
'@rolldown/binding-darwin-x64': 1.0.0-rc.6
|
'@rolldown/binding-darwin-x64': 1.0.0-rc.8
|
||||||
'@rolldown/binding-freebsd-x64': 1.0.0-rc.6
|
'@rolldown/binding-freebsd-x64': 1.0.0-rc.8
|
||||||
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.6
|
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.8
|
||||||
'@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.6
|
'@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.8
|
||||||
'@rolldown/binding-linux-arm64-musl': 1.0.0-rc.6
|
'@rolldown/binding-linux-arm64-musl': 1.0.0-rc.8
|
||||||
'@rolldown/binding-linux-x64-gnu': 1.0.0-rc.6
|
'@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.8
|
||||||
'@rolldown/binding-linux-x64-musl': 1.0.0-rc.6
|
'@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.8
|
||||||
'@rolldown/binding-openharmony-arm64': 1.0.0-rc.6
|
'@rolldown/binding-linux-x64-gnu': 1.0.0-rc.8
|
||||||
'@rolldown/binding-wasm32-wasi': 1.0.0-rc.6
|
'@rolldown/binding-linux-x64-musl': 1.0.0-rc.8
|
||||||
'@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.6
|
'@rolldown/binding-openharmony-arm64': 1.0.0-rc.8
|
||||||
'@rolldown/binding-win32-x64-msvc': 1.0.0-rc.6
|
'@rolldown/binding-wasm32-wasi': 1.0.0-rc.8
|
||||||
|
'@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.8
|
||||||
|
'@rolldown/binding-win32-x64-msvc': 1.0.0-rc.8
|
||||||
|
|
||||||
scheduler@0.27.0: {}
|
scheduler@0.27.0: {}
|
||||||
|
|
||||||
@@ -3978,13 +3995,13 @@ snapshots:
|
|||||||
|
|
||||||
uuid@8.3.2: {}
|
uuid@8.3.2: {}
|
||||||
|
|
||||||
vite@8.0.0-beta.16(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0):
|
vite@8.0.0-beta.18(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@oxc-project/runtime': 0.115.0
|
'@oxc-project/runtime': 0.115.0
|
||||||
lightningcss: 1.31.1
|
lightningcss: 1.31.1
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.3
|
||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
rolldown: 1.0.0-rc.6
|
rolldown: 1.0.0-rc.8
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 25.4.0
|
'@types/node': 25.4.0
|
||||||
|
|||||||
55
src/modules/asr-api.ts
Normal file
55
src/modules/asr-api.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { createQueryApi } from '@kevisual/query/api';
|
||||||
|
import { query } from '@/modules/query.ts';
|
||||||
|
const api = {
|
||||||
|
"asr": {
|
||||||
|
/**
|
||||||
|
* 语音转文字,将base64的音频数据转换为文字, 参数: base64Data 为base64编码的音频数据
|
||||||
|
*
|
||||||
|
* @param data - Request parameters
|
||||||
|
* @param data.base64Data - {string (minLength: 1)} base64编码的音频数据
|
||||||
|
*/
|
||||||
|
"text": {
|
||||||
|
"path": "asr",
|
||||||
|
"key": "text",
|
||||||
|
"description": "语音转文字,将base64的音频数据转换为文字, 参数: base64Data 为base64编码的音频数据",
|
||||||
|
"metadata": {
|
||||||
|
"args": {
|
||||||
|
"base64Data": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"description": "base64编码的音频数据"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": "/api/router",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 语音转文字,将音频链接的音频数据转换为文字, 参数: url 为音频链接
|
||||||
|
*
|
||||||
|
* @param data - Request parameters
|
||||||
|
* @param data.url - {string (minLength: 1)} 音频链接
|
||||||
|
*/
|
||||||
|
"link": {
|
||||||
|
"path": "asr",
|
||||||
|
"key": "link",
|
||||||
|
"description": "语音转文字,将音频链接的音频数据转换为文字, 参数: url 为音频链接",
|
||||||
|
"metadata": {
|
||||||
|
"args": {
|
||||||
|
"url": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"description": "音频链接"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": "/api/router",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
const queryApi = createQueryApi({ api, query });
|
||||||
|
|
||||||
|
export { queryApi };
|
||||||
281
src/modules/flowme-api.ts
Normal file
281
src/modules/flowme-api.ts
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
import { createQueryApi } from '@kevisual/query/api';
|
||||||
|
import { query } from '@/modules/query.ts';
|
||||||
|
const api = {
|
||||||
|
"flowme": {
|
||||||
|
/**
|
||||||
|
* 获取 flowme 列表
|
||||||
|
*
|
||||||
|
* @param data - Request parameters
|
||||||
|
* @param data.page - {number} 页码, 默认为 1
|
||||||
|
* @param data.pageSize - {number} 每页数量, 默认为 100
|
||||||
|
* @param data.search - {string} 搜索关键词
|
||||||
|
* @param data.channelId - {string} 频道ID
|
||||||
|
* @param data.type - {string} 类型
|
||||||
|
* @param data.sort - {"ASC" | "DESC"} 排序方式,ASC 或 DESC,默认为 DESC
|
||||||
|
* @param data.timeRange - {object} 时间范围过滤
|
||||||
|
*/
|
||||||
|
"list": {
|
||||||
|
"path": "flowme",
|
||||||
|
"key": "list",
|
||||||
|
"description": "获取 flowme 列表",
|
||||||
|
"metadata": {
|
||||||
|
"args": {
|
||||||
|
"page": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "number",
|
||||||
|
"description": "页码, 默认为 1",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"pageSize": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "number",
|
||||||
|
"description": "每页数量, 默认为 100",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"description": "搜索关键词",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"channelId": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"description": "频道ID",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"description": "类型",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sort": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"ASC",
|
||||||
|
"DESC"
|
||||||
|
],
|
||||||
|
"description": "排序方式,ASC 或 DESC,默认为 DESC",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"timeRange": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"from": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "开始时间,ISO 格式"
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "结束时间,ISO 格式"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"description": "时间范围过滤",
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": "/api/router",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 创建一个 flowme
|
||||||
|
*
|
||||||
|
* @param data - Request parameters
|
||||||
|
* @param data.data - {object}
|
||||||
|
*/
|
||||||
|
"create": {
|
||||||
|
"path": "flowme",
|
||||||
|
"key": "create",
|
||||||
|
"description": "创建一个 flowme",
|
||||||
|
"metadata": {
|
||||||
|
"args": {
|
||||||
|
"data": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "标题"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "描述"
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "标签"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "链接"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"propertyNames": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"additionalProperties": {},
|
||||||
|
"description": "数据"
|
||||||
|
},
|
||||||
|
"channelId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "频道ID"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "类型"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "来源"
|
||||||
|
},
|
||||||
|
"importance": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "重要性等级"
|
||||||
|
},
|
||||||
|
"isArchived": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "是否归档"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": "/api/router",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 更新一个 flowme
|
||||||
|
*
|
||||||
|
* @param data - Request parameters
|
||||||
|
* @param data.data - {object}
|
||||||
|
*/
|
||||||
|
"update": {
|
||||||
|
"path": "flowme",
|
||||||
|
"key": "update",
|
||||||
|
"description": "更新一个 flowme",
|
||||||
|
"metadata": {
|
||||||
|
"args": {
|
||||||
|
"data": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "标题"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "描述"
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "标签"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "链接"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"propertyNames": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"additionalProperties": {},
|
||||||
|
"description": "数据"
|
||||||
|
},
|
||||||
|
"channelId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "频道ID"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "类型"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "来源"
|
||||||
|
},
|
||||||
|
"importance": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "重要性等级"
|
||||||
|
},
|
||||||
|
"isArchived": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "是否归档"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": "/api/router",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 删除 flowme
|
||||||
|
*
|
||||||
|
* @param data - Request parameters
|
||||||
|
* @param data.data - {object}
|
||||||
|
*/
|
||||||
|
"delete": {
|
||||||
|
"path": "flowme",
|
||||||
|
"key": "delete",
|
||||||
|
"description": "删除 flowme ",
|
||||||
|
"metadata": {
|
||||||
|
"args": {
|
||||||
|
"data": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": "/api/router",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 获取单个 flowme, 参数: data.id 必填
|
||||||
|
*/
|
||||||
|
"get": {
|
||||||
|
"path": "flowme",
|
||||||
|
"key": "get",
|
||||||
|
"description": "获取单个 flowme, 参数: data.id 必填",
|
||||||
|
"metadata": {
|
||||||
|
"url": "/api/router",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
const queryApi = createQueryApi({ api, query });
|
||||||
|
|
||||||
|
export { queryApi };
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import { VadVoice } from './modules/VadVoice';
|
import { VadVoice } from './modules/VadVoice';
|
||||||
|
import { ConfirmDeleteModal } from './modules/ConfirmDeleteModal';
|
||||||
|
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
return <div className="h-full overflow-hidden">
|
return <div className="h-full overflow-hidden">
|
||||||
<VadVoice />
|
<VadVoice />
|
||||||
|
<ConfirmDeleteModal />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
55
src/pages/muse/voice/modules/ConfirmDeleteModal.tsx
Normal file
55
src/pages/muse/voice/modules/ConfirmDeleteModal.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useVoiceStore } from "../store/voiceStore";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
export const ConfirmDeleteModal = () => {
|
||||||
|
const { deleteConfirm, closeDeleteConfirm } = useVoiceStore();
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (!deleteConfirm.voice) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { deleteVoice } = useVoiceStore.getState();
|
||||||
|
await deleteVoice(deleteConfirm.voice);
|
||||||
|
console.log("语音记录删除成功");
|
||||||
|
toast.success("删除成功");
|
||||||
|
closeDeleteConfirm();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("删除语音记录失败:", error);
|
||||||
|
toast.error("删除失败: " + (error instanceof Error ? error.message : "未知错误"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const speak = deleteConfirm.voice?.data?.speak;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={deleteConfirm.open} onOpenChange={(open) => !open && closeDeleteConfirm()}>
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>确认删除</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{speak?.text
|
||||||
|
? `此语音包含文字内容:"${speak.text}"。删除后将无法恢复,确定要删除吗?`
|
||||||
|
: "确定要删除这条语音记录吗?删除后将无法恢复。"}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={closeDeleteConfirm}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button variant="destructive" onClick={handleDelete}>
|
||||||
|
确认删除
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -7,21 +7,16 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "../../../../components/ui/dialog";
|
} from "../../../../components/ui/dialog";
|
||||||
import { useSettingStore } from '../store/settingStore';
|
import { useSettingStore } from '../store/settingStore';
|
||||||
import { PasswordInput } from '../../components/PasswordInput';
|
import { X } from 'lucide-react';
|
||||||
import { X, RotateCcw } from 'lucide-react';
|
|
||||||
|
|
||||||
export const SettingModal: React.FC = () => {
|
export const SettingModal: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
isOpen,
|
isOpen,
|
||||||
autoRecognize,
|
autoRecognize,
|
||||||
listen,
|
listen,
|
||||||
volcengineAucAppId,
|
|
||||||
volcengineAucToken,
|
|
||||||
closeModal,
|
closeModal,
|
||||||
setAutoRecognize,
|
setAutoRecognize,
|
||||||
setListen,
|
setListen,
|
||||||
setVolcengineAucAppId,
|
|
||||||
setVolcengineAucToken,
|
|
||||||
resetToDefault,
|
resetToDefault,
|
||||||
} = useSettingStore();
|
} = useSettingStore();
|
||||||
|
|
||||||
@@ -104,36 +99,7 @@ export const SettingModal: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 火山引擎配置 */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-sm font-medium text-gray-900">火山引擎配置</h3>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm text-gray-700 mb-1">App ID</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={volcengineAucAppId}
|
|
||||||
onChange={(e) => setVolcengineAucAppId(e.target.value)}
|
|
||||||
placeholder="请输入火山引擎 App ID"
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm text-gray-700 mb-1">Token</label>
|
|
||||||
<PasswordInput
|
|
||||||
value={volcengineAucToken}
|
|
||||||
onChange={setVolcengineAucToken}
|
|
||||||
placeholder="请输入火山引擎 Token"
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end space-x-3 pt-4 border-t">
|
<div className="flex justify-end space-x-3 pt-4 border-t">
|
||||||
<button
|
<button
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
|
|||||||
@@ -2,40 +2,30 @@ import { MicVAD, utils } from "@ricky0123/vad-web"
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import './style.css'
|
import './style.css'
|
||||||
import { MoreHorizontal, Play, Pause, Settings, FileAudio, StopCircle, Loader, Copy } from "lucide-react";
|
import { MoreHorizontal, Play, Pause, Settings, FileAudio, StopCircle, Loader } from "lucide-react";
|
||||||
import { Menu, MenuItem, MenuButton, } from '@szhsin/react-menu';
|
import { Menu, MenuItem, MenuButton, } from '@szhsin/react-menu';
|
||||||
import '@szhsin/react-menu/dist/index.css';
|
import '@szhsin/react-menu/dist/index.css';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Speak } from "./speak-db/speak";
|
import { FlowmeSpeak, useVoiceStore } from "../store/voiceStore";
|
||||||
import { useVoiceStore } from "../store/voiceStore";
|
|
||||||
import { useSettingStore } from "../store/settingStore";
|
import { useSettingStore } from "../store/settingStore";
|
||||||
import { SettingModal } from "./SettingModal";
|
import { SettingModal } from "./SettingModal";
|
||||||
import { AudioRecorder } from "./AudioRecorder";
|
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
} from "@/components/ui/alert-dialog";
|
|
||||||
import { ShowText } from "./ShowText";
|
import { ShowText } from "./ShowText";
|
||||||
|
import { useResourceStore } from "../store/resourceStore";
|
||||||
|
import { useShallow } from "zustand/shallow";
|
||||||
|
import { useLayoutStore } from "@/pages/auth/store";
|
||||||
type VadVoiceProps = {
|
type VadVoiceProps = {
|
||||||
data: Speak;
|
item: FlowmeSpeak;
|
||||||
}
|
}
|
||||||
const VoicePlayer = ({ data }: VadVoiceProps) => {
|
const VoicePlayer = ({ item }: VadVoiceProps) => {
|
||||||
|
const data = item.data.speak || {};
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [audio, setAudio] = useState<HTMLAudioElement | null>(null);
|
const [audio, setAudio] = useState<HTMLAudioElement | null>(null);
|
||||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data.url) return;
|
const url = item.link;
|
||||||
|
|
||||||
const audioInstance = new Audio(data.url);
|
const audioInstance = new Audio(url);
|
||||||
audioRef.current = audioInstance;
|
audioRef.current = audioInstance;
|
||||||
setAudio(audioInstance);
|
setAudio(audioInstance);
|
||||||
|
|
||||||
@@ -51,7 +41,7 @@ const VoicePlayer = ({ data }: VadVoiceProps) => {
|
|||||||
audioInstance.pause();
|
audioInstance.pause();
|
||||||
audioInstance.src = '';
|
audioInstance.src = '';
|
||||||
};
|
};
|
||||||
}, [data.url]);
|
}, [item]);
|
||||||
|
|
||||||
const handlePlay = () => {
|
const handlePlay = () => {
|
||||||
if (audio) {
|
if (audio) {
|
||||||
@@ -76,27 +66,15 @@ const VoicePlayer = ({ data }: VadVoiceProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDownload = () => {
|
const handleDownload = () => {
|
||||||
if (!data.url) return;
|
if (!item.link) return;
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = data.url;
|
link.href = item.link;
|
||||||
link.download = `recording-${data.timestamp}.wav`;
|
link.download = `recording-${data.timestamp}.wav`;
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = () => {
|
|
||||||
const { deleteVoice } = useVoiceStore.getState();
|
|
||||||
deleteVoice(data.id)
|
|
||||||
.then(() => {
|
|
||||||
console.log('语音记录删除成功');
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('删除语音记录失败:', error);
|
|
||||||
toast.error('删除失败: ' + (error instanceof Error ? error.message : '未知错误'));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRecognize = () => {
|
const handleRecognize = () => {
|
||||||
if (data.text && data.text.trim()) {
|
if (data.text && data.text.trim()) {
|
||||||
toast.info('该语音记录已经识别过了,文字内容:' + data.text);
|
toast.info('该语音记录已经识别过了,文字内容:' + data.text);
|
||||||
@@ -104,22 +82,14 @@ const VoicePlayer = ({ data }: VadVoiceProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { recognizeVoice } = useVoiceStore.getState();
|
const { recognizeVoice } = useVoiceStore.getState();
|
||||||
recognizeVoice(data.id)
|
recognizeVoice(item)
|
||||||
.then((text) => {
|
|
||||||
console.log('语音识别成功:', text);
|
|
||||||
toast.success('识别成功!文字内容:' + text);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('语音识别失败:', error);
|
|
||||||
toast.error('识别失败: ' + (error instanceof Error ? error.message : '未知错误'));
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
|
|
||||||
{/* 工具菜单 */}
|
{/* 工具菜单 */}
|
||||||
<Menu menuButton={
|
<Menu portal={false} menuButton={
|
||||||
<MenuButton className="w-8 h-8 hover:bg-gray-400 rounded flex items-center justify-center text-blank cursor-pointer">
|
<MenuButton className="w-8 h-8 hover:bg-gray-400 rounded flex items-center justify-center text-blank cursor-pointer">
|
||||||
<MoreHorizontal className="w-4 h-4" />
|
<MoreHorizontal className="w-4 h-4" />
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
@@ -150,51 +120,14 @@ const VoicePlayer = ({ data }: VadVoiceProps) => {
|
|||||||
<span>识别语音</span>
|
<span>识别语音</span>
|
||||||
</div>
|
</div>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{data.text && data.text.trim() ? (
|
<MenuItem onClick={() => useVoiceStore.getState().openDeleteConfirm(item)}>
|
||||||
<AlertDialog>
|
<div className="flex items-center space-x-2">
|
||||||
<AlertDialogTrigger asChild>
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<MenuItem>
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1-1H8a1 1 0 00-1 1v3M4 7h16" />
|
||||||
<div className="flex items-center space-x-2">
|
</svg>
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<span>删除</span>
|
||||||
<path
|
</div>
|
||||||
strokeLinecap="round"
|
</MenuItem>
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1-1H8a1 1 0 00-1 1v3M4 7h16"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>删除</span>
|
|
||||||
</div>
|
|
||||||
</MenuItem>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
此语音包含文字内容:"{data.text}"。删除后将无法恢复,确定要删除吗?
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
|
||||||
<AlertDialogAction onClick={handleDelete}>确认删除</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
) : (
|
|
||||||
<MenuItem onClick={handleDelete}>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1-1H8a1 1 0 00-1 1v3M4 7h16"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>删除</span>
|
|
||||||
</div>
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
</Menu>
|
</Menu>
|
||||||
{/* 播放/暂停按钮 */}
|
{/* 播放/暂停按钮 */}
|
||||||
{!isPlaying ? (
|
{!isPlaying ? (
|
||||||
@@ -223,7 +156,69 @@ const VoicePlayer = ({ data }: VadVoiceProps) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export const ShowVoicePlayer = ({ data }: { data: Speak[] }) => {
|
const ShowVoicePlayerItem = ({ item }: { item: FlowmeSpeak }) => {
|
||||||
|
const speak = item.data.speak || {};
|
||||||
|
return <li className="bg-white rounded-lg border border-gray-200 p-2 hover:shadow-sm transition-shadow">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2 flex-1 min-w-0">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<VoicePlayer item={item} />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center justify-between space-x-2">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-gray-400 truncate">
|
||||||
|
{speak ? new Date(speak.timestamp).toLocaleTimeString() : ''}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-300">
|
||||||
|
#{speak?.no}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<div
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
useVoiceStore.getState().openDeleteConfirm(item);
|
||||||
|
}}
|
||||||
|
className="w-5 h-5 hover:bg-red-100 rounded flex items-center justify-center text-red-500 transition-colors cursor-pointer"
|
||||||
|
title="删除"
|
||||||
|
>
|
||||||
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1-1H8a1 1 0 00-1 1v3M4 7h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{speak.text && (
|
||||||
|
<div
|
||||||
|
className="text-xs text-gray-600 mt-1 truncate cursor-pointer hover:bg-gray-100 rounded px-1 transition-colors"
|
||||||
|
title={`${speak.text} (点击复制)`}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!speak.text) return;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(speak.text);
|
||||||
|
toast.success('文字已复制到剪贴板');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('复制失败:', error);
|
||||||
|
toast.error('复制失败,请手动选择文字复制');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
📝 {speak.text}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
export const ShowVoicePlayer = ({ data }: { data: FlowmeSpeak[] }) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const bottomElement = document.getElementById('voice-list-bottom');
|
const bottomElement = document.getElementById('voice-list-bottom');
|
||||||
if (bottomElement) {
|
if (bottomElement) {
|
||||||
@@ -231,124 +226,8 @@ export const ShowVoicePlayer = ({ data }: { data: Speak[] }) => {
|
|||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
return (<ul className="space-y-2 max-h-full">
|
return (<ul className="space-y-2 max-h-full">
|
||||||
{data.map((item, index) => (
|
{data.map((item) => (
|
||||||
<li key={index} className="bg-white rounded-lg border border-gray-200 p-2 hover:shadow-sm transition-shadow">
|
<ShowVoicePlayerItem key={item.id} item={item} />
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center space-x-2 flex-1 min-w-0">
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<VoicePlayer data={item} />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-center justify-between space-x-2">
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-gray-400 truncate">
|
|
||||||
{new Date(item.timestamp).toLocaleTimeString()}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-300">
|
|
||||||
#{item.no}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
{item.text && item.text.trim() ? (
|
|
||||||
<AlertDialog>
|
|
||||||
<AlertDialogTrigger asChild>
|
|
||||||
<button
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
className="w-5 h-5 hover:bg-red-100 rounded flex items-center justify-center text-red-500 transition-colors cursor-pointer"
|
|
||||||
title="删除"
|
|
||||||
>
|
|
||||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1-1H8a1 1 0 00-1 1v3M4 7h16"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
此语音包含文字内容:"{item.text}"。删除后将无法恢复,确定要删除吗?
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const { deleteVoice } = useVoiceStore.getState();
|
|
||||||
deleteVoice(item.id)
|
|
||||||
.then(() => {
|
|
||||||
console.log('语音记录删除成功');
|
|
||||||
toast.success('删除成功', { autoClose: 200 });
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('删除语音记录失败:', error);
|
|
||||||
toast.error('删除失败: ' + (error instanceof Error ? error.message : '未知错误'));
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
确认删除
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const { deleteVoice } = useVoiceStore.getState();
|
|
||||||
deleteVoice(item.id)
|
|
||||||
.then(() => {
|
|
||||||
console.log('语音记录删除成功');
|
|
||||||
toast.success('删除成功', { autoClose: 200 });
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('删除语音记录失败:', error);
|
|
||||||
toast.error('删除失败: ' + (error instanceof Error ? error.message : '未知错误'));
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className="w-5 h-5 hover:bg-red-100 rounded flex items-center justify-center text-red-500 transition-colors cursor-pointer"
|
|
||||||
title="删除"
|
|
||||||
>
|
|
||||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1-1H8a1 1 0 00-1 1v3M4 7h16"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{item.text && (
|
|
||||||
<div
|
|
||||||
className="text-xs text-gray-600 mt-1 truncate cursor-pointer hover:bg-gray-100 rounded px-1 transition-colors"
|
|
||||||
title={`${item.text} (点击复制)`}
|
|
||||||
onClick={async (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (!item.text) return;
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(item.text);
|
|
||||||
toast.success('文字已复制到剪贴板', { autoClose: 1000 });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('复制失败:', error);
|
|
||||||
toast.error('复制失败,请手动选择文字复制');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
📝 {item.text}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div id="voice-list-bottom" />
|
<div id="voice-list-bottom" />
|
||||||
@@ -362,12 +241,9 @@ export const VadVoice = () => {
|
|||||||
error: storeError,
|
error: storeError,
|
||||||
initialize: initializeStore,
|
initialize: initializeStore,
|
||||||
addVoice,
|
addVoice,
|
||||||
|
showText,
|
||||||
setError: setStoreError,
|
setError: setStoreError,
|
||||||
relatimeParialText,
|
|
||||||
relatimeFinalText,
|
|
||||||
lastRecognizedText
|
|
||||||
} = useVoiceStore();
|
} = useVoiceStore();
|
||||||
const showText = relatimeFinalText || relatimeParialText;
|
|
||||||
// 使用设置 store
|
// 使用设置 store
|
||||||
const {
|
const {
|
||||||
openModal: openSettingModal,
|
openModal: openSettingModal,
|
||||||
@@ -376,13 +252,14 @@ export const VadVoice = () => {
|
|||||||
autoRecognize,
|
autoRecognize,
|
||||||
setAutoRecognize
|
setAutoRecognize
|
||||||
} = useSettingStore();
|
} = useSettingStore();
|
||||||
|
const layoutStore = useLayoutStore(useShallow(state => ({ me: state.me })));
|
||||||
|
const resourceStore = useResourceStore(useShallow(state => ({ init: state.init })));
|
||||||
|
|
||||||
const [vadStatus, setVadStatus] = useState<'idle' | 'initializing' | 'ready' | 'error'>('idle');
|
const [vadStatus, setVadStatus] = useState<'idle' | 'initializing' | 'ready' | 'error'>('idle');
|
||||||
const [realListen, setRealListen] = useState<boolean>(false);
|
const [realListen, setRealListen] = useState<boolean>(false);
|
||||||
const [errorMessage, setErrorMessage] = useState<string>('');
|
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||||
const [userInteracted, setUserInteracted] = useState<boolean>(false);
|
const [userInteracted, setUserInteracted] = useState<boolean>(false);
|
||||||
const ref = useRef<MicVAD | null>(null);
|
const ref = useRef<MicVAD | null>(null);
|
||||||
const audioRecorderRef = useRef<AudioRecorder | null>(null);
|
|
||||||
const initializingRef = useRef<boolean>(false);
|
const initializingRef = useRef<boolean>(false);
|
||||||
|
|
||||||
async function initializeVAD(ls: boolean = true) {
|
async function initializeVAD(ls: boolean = true) {
|
||||||
@@ -399,26 +276,9 @@ export const VadVoice = () => {
|
|||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
|
||||||
const myvad = await MicVAD.new({
|
const myvad = await MicVAD.new({
|
||||||
getStream: async () => {
|
|
||||||
const audioRecorder = audioRecorderRef.current || new AudioRecorder({
|
|
||||||
sampleRate: 16000,
|
|
||||||
bufferSize: 4096,
|
|
||||||
});
|
|
||||||
await audioRecorder.start();
|
|
||||||
// 设置音频数据回调
|
|
||||||
audioRecorder.onAudioData((audioData) => {
|
|
||||||
const base64 = AudioRecorder.float32ArrayToBase64(audioData);
|
|
||||||
const relatime = useVoiceStore.getState().relatime;
|
|
||||||
relatime?.sendBase64(base64, { isRelatime: true });
|
|
||||||
});
|
|
||||||
audioRecorderRef.current = audioRecorder;
|
|
||||||
return audioRecorder.getMediaStream()!;
|
|
||||||
},
|
|
||||||
onSpeechEnd: async (audio) => {
|
onSpeechEnd: async (audio) => {
|
||||||
try {
|
try {
|
||||||
const wavBuffer = utils.encodeWAV(audio)
|
const wavBuffer = utils.encodeWAV(audio)
|
||||||
const relatime = useVoiceStore.getState().relatime;
|
|
||||||
relatime?.sendBase64?.(utils.arrayBufferToBase64(wavBuffer), { isRelatime: false });
|
|
||||||
const audioBlob = new Blob([wavBuffer], { type: 'audio/wav' })
|
const audioBlob = new Blob([wavBuffer], { type: 'audio/wav' })
|
||||||
const tempUrl = URL.createObjectURL(audioBlob)
|
const tempUrl = URL.createObjectURL(audioBlob)
|
||||||
|
|
||||||
@@ -444,13 +304,11 @@ export const VadVoice = () => {
|
|||||||
setRealListen(false);
|
setRealListen(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onnxWASMBasePath: "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.22.0/dist/",
|
onnxWASMBasePath: "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.24.3/dist/",
|
||||||
baseAssetPath: "https://cdn.jsdelivr.net/npm/@ricky0123/vad-web@0.0.27/dist/",
|
baseAssetPath: "https://cdn.jsdelivr.net/npm/@ricky0123/vad-web@0.0.30/dist/",
|
||||||
onSpeechRealStart: () => {
|
onSpeechRealStart: () => {
|
||||||
console.log('VAD real start');
|
console.log('VAD real start');
|
||||||
setRealListen(true);
|
setRealListen(true);
|
||||||
const relatime = useVoiceStore.getState().relatime;
|
|
||||||
relatime?.setStartTime?.(Date.now());
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -483,6 +341,10 @@ export const VadVoice = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initializeStore();
|
initializeStore();
|
||||||
}, [initializeStore]);
|
}, [initializeStore]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!layoutStore.me?.id) return;
|
||||||
|
resourceStore.init(layoutStore.me.username!);
|
||||||
|
}, [layoutStore.me])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 页面加载时不自动初始化,等待用户交互
|
// 页面加载时不自动初始化,等待用户交互
|
||||||
const handleFirstClick = () => {
|
const handleFirstClick = () => {
|
||||||
@@ -521,8 +383,6 @@ export const VadVoice = () => {
|
|||||||
ref.current = null;
|
ref.current = null;
|
||||||
setListen(false);
|
setListen(false);
|
||||||
setVadStatus('idle');
|
setVadStatus('idle');
|
||||||
audioRecorderRef.current?.stop();
|
|
||||||
audioRecorderRef.current = null;
|
|
||||||
console.log('VAD closed');
|
console.log('VAD closed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -604,8 +464,7 @@ export const VadVoice = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{lastRecognizedText && <ShowText text={lastRecognizedText} title="上次识别" icon={'🕘'} />}
|
{showText && <ShowText text={showText} icon={'📝'} />}
|
||||||
<ShowText text={showText} icon={'📝'} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { nanoid } from "nanoid";
|
|||||||
export type Speak = {
|
export type Speak = {
|
||||||
id: string;
|
id: string;
|
||||||
no: number; // 序号, 当天的序号
|
no: number; // 序号, 当天的序号
|
||||||
file?: string; // base64 编码的音频文件
|
|
||||||
text?: string; // 文字内容,识别的内容
|
text?: string; // 文字内容,识别的内容
|
||||||
timestamp: number; // 生成时间戳
|
timestamp: number; // 生成时间戳
|
||||||
day: number; // 365天中的第几天
|
day: number; // 365天中的第几天
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
// Store exports
|
// Store exports
|
||||||
export { useVoiceStore, useVoiceList, useVoiceLoading, useVoiceError, cleanupVoiceUrls } from './voiceStore';
|
export { useVoiceStore } from './voiceStore';
|
||||||
export type { VoiceState } from './voiceStore';
|
export type { VoiceState } from './voiceStore';
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import { WSServer } from '@kevisual/video-tools/src/asr/ws.ts'
|
|
||||||
import { useVoiceStore } from './voiceStore'
|
|
||||||
|
|
||||||
export class Relatime {
|
|
||||||
asr: WSServer
|
|
||||||
ready = false
|
|
||||||
timeoutHandle: NodeJS.Timeout | null = null
|
|
||||||
startTime: number = 0
|
|
||||||
isRelatime: boolean = true
|
|
||||||
constructor() {
|
|
||||||
// const url = new URL('/ws/asr', "http://localhost:51015")
|
|
||||||
const url = new URL('/ws/asr', window.location.origin)
|
|
||||||
url.searchParams.set('id', 'muse-voice-relatime')
|
|
||||||
const ws = new WSServer({
|
|
||||||
url: url.toString(),
|
|
||||||
onConnect: () => {
|
|
||||||
console.log('WebSocket connected');
|
|
||||||
ws.emitter.on("message", (data) => {
|
|
||||||
// console.log("Received message:", data.data);
|
|
||||||
const json = JSON.parse(data.data);
|
|
||||||
console.log('json', json);
|
|
||||||
if (json && json.type === 'connected') {
|
|
||||||
ws.ws.send(JSON.stringify({ type: 'init' }));
|
|
||||||
}
|
|
||||||
if (json && json.type === 'asr' && json.code === 200) {
|
|
||||||
ws.emitter.emit('asr');
|
|
||||||
}
|
|
||||||
if (json && json.type === 'partial' || json.type === 'result') {
|
|
||||||
const text = json.text || '';
|
|
||||||
const isPartial = json.type === 'partial';
|
|
||||||
const isResult = json.type === 'result';
|
|
||||||
if (isPartial) {
|
|
||||||
// 部分结果
|
|
||||||
useVoiceStore.getState().setRelatimeParialText(text);
|
|
||||||
} else {
|
|
||||||
// 最终结果
|
|
||||||
useVoiceStore.getState().setRelatimeFinalText(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ws.emitter.once('asr', async () => {
|
|
||||||
console.log('ASR ready');
|
|
||||||
this.ready = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.asr = ws
|
|
||||||
}
|
|
||||||
send(data: Buffer) {
|
|
||||||
if (!this.ready) return;
|
|
||||||
const voice = data.toString('base64');
|
|
||||||
this.asr.ws.send(JSON.stringify({ voice }));
|
|
||||||
}
|
|
||||||
setIsRelatime(isRelatime: boolean) {
|
|
||||||
this.isRelatime = isRelatime;
|
|
||||||
}
|
|
||||||
async sendBase64(data: string, opts?: { isRelatime?: boolean }) {
|
|
||||||
if (!this.ready) return;
|
|
||||||
if (opts?.isRelatime !== this.isRelatime) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// console.log('send 花费时间:', Date.now() - this.startTime);
|
|
||||||
const connected = await this.asr.checkConnected();
|
|
||||||
if (!connected) return;
|
|
||||||
this.asr.ws.send(JSON.stringify({ voice: data, format: 'float32', time: Date.now(), ...opts }));
|
|
||||||
// if (this.timeoutHandle) {
|
|
||||||
// clearTimeout(this.timeoutHandle);
|
|
||||||
// }
|
|
||||||
// this.timeoutHandle = setTimeout(() => {
|
|
||||||
// this.asr.sendBlankJson()
|
|
||||||
// this.timeoutHandle = null;
|
|
||||||
// }, 20000); // 20秒钟没有数据则发送空JSON保持连接
|
|
||||||
}
|
|
||||||
setStartTime(time: number) {
|
|
||||||
this.startTime = time;
|
|
||||||
}
|
|
||||||
showCostTime() {
|
|
||||||
console.log('当前花费时间:', Date.now() - this.startTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
59
src/pages/muse/voice/store/resourceStore.ts
Normal file
59
src/pages/muse/voice/store/resourceStore.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { QueryResources } from '@kevisual/api/query-resources'
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { Speak } from '../modules/speak-db';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { queryApi as flowmeApi } from '@/modules/flowme-api';
|
||||||
|
|
||||||
|
const generateId = (): string => {
|
||||||
|
return `speak_${dayjs().format('HHmm')}_${Math.random().toString(36).substring(2, 8)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const queryResources = new QueryResources({
|
||||||
|
prefix: 'muse/voice',
|
||||||
|
onProcess: (opts) => {
|
||||||
|
console.log('QueryResources process:', opts);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
interface ResourceState {
|
||||||
|
init: (username: string) => Promise<void>;
|
||||||
|
mount: boolean;
|
||||||
|
upload: (speakData: Speak, blob?: Blob) => Promise<void>;
|
||||||
|
}
|
||||||
|
export const useResourceStore = create<ResourceState>((set, get) => ({
|
||||||
|
mount: false,
|
||||||
|
init: async (username: string) => {
|
||||||
|
queryResources.setPrefix(`/${username}/resources/upload/1.0.0/voice/`);
|
||||||
|
set({ mount: true });
|
||||||
|
},
|
||||||
|
upload: async (speakData: Speak, blob?: Blob) => {
|
||||||
|
const mount = get().mount;
|
||||||
|
if (!mount) {
|
||||||
|
console.warn('初始化资源上传服务,请先调用 init 方法');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
console.log('上传资源:', speakData, blob);
|
||||||
|
const today = dayjs().format('YYYY/MMDD');
|
||||||
|
const afterLink = today + '/' + generateId() + '.wav';
|
||||||
|
const resourceUrl = queryResources.prefix + afterLink;
|
||||||
|
console.log('资源 URL:', resourceUrl);
|
||||||
|
const res = await flowmeApi.flowme.create({
|
||||||
|
data: {
|
||||||
|
title: '由 Muse 语音模块上传的资源',
|
||||||
|
tags: ['muse', 'voice'],
|
||||||
|
link: resourceUrl,
|
||||||
|
type: 'muse',
|
||||||
|
data: {
|
||||||
|
type: 'voice',
|
||||||
|
speak: speakData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('Flowme 创建资源结果:', res);
|
||||||
|
await queryResources.uploadFile(afterLink, blob!)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('资源上传失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
@@ -10,18 +10,12 @@ interface SettingState {
|
|||||||
listen: boolean;
|
listen: boolean;
|
||||||
recognitionLanguage: string;
|
recognitionLanguage: string;
|
||||||
|
|
||||||
// 火山引擎配置
|
|
||||||
volcengineAucAppId: string;
|
|
||||||
volcengineAucToken: string;
|
|
||||||
|
|
||||||
// 操作方法
|
// 操作方法
|
||||||
openModal: () => void;
|
openModal: () => void;
|
||||||
closeModal: () => void;
|
closeModal: () => void;
|
||||||
setAutoRecognize: (value: boolean) => void;
|
setAutoRecognize: (value: boolean) => void;
|
||||||
setListen: (value: boolean) => void;
|
setListen: (value: boolean) => void;
|
||||||
setRecognitionLanguage: (language: string) => void;
|
setRecognitionLanguage: (language: string) => void;
|
||||||
setVolcengineAucAppId: (appId: string) => void;
|
|
||||||
setVolcengineAucToken: (token: string) => void;
|
|
||||||
resetToDefault: () => void;
|
resetToDefault: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,24 +23,6 @@ const defaultSettings = {
|
|||||||
autoRecognize: false,
|
autoRecognize: false,
|
||||||
listen: false,
|
listen: false,
|
||||||
recognitionLanguage: 'zh-CN',
|
recognitionLanguage: 'zh-CN',
|
||||||
volcengineAucAppId: '',
|
|
||||||
volcengineAucToken: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
// 从原有的 localStorage key 读取初始值
|
|
||||||
const getInitialVolcengineConfig = () => {
|
|
||||||
try {
|
|
||||||
return {
|
|
||||||
volcengineAucAppId: localStorage.getItem('VOLCENGINE_AUC_APPID') || '',
|
|
||||||
volcengineAucToken: localStorage.getItem('VOLCENGINE_AUC_TOKEN') || '',
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to read volcengine config from localStorage:', error);
|
|
||||||
return {
|
|
||||||
volcengineAucAppId: '',
|
|
||||||
volcengineAucToken: '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSettingStore = create<SettingState>()(
|
export const useSettingStore = create<SettingState>()(
|
||||||
@@ -55,7 +31,6 @@ export const useSettingStore = create<SettingState>()(
|
|||||||
// 初始状态 - 合并默认设置和从 localStorage 读取的火山引擎配置
|
// 初始状态 - 合并默认设置和从 localStorage 读取的火山引擎配置
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
...defaultSettings,
|
...defaultSettings,
|
||||||
...getInitialVolcengineConfig(),
|
|
||||||
mount: false,
|
mount: false,
|
||||||
// 弹窗控制方法
|
// 弹窗控制方法
|
||||||
openModal: () => set({ isOpen: true }),
|
openModal: () => set({ isOpen: true }),
|
||||||
@@ -65,37 +40,12 @@ export const useSettingStore = create<SettingState>()(
|
|||||||
setAutoRecognize: (value: boolean) => set({ autoRecognize: value }),
|
setAutoRecognize: (value: boolean) => set({ autoRecognize: value }),
|
||||||
setListen: (value: boolean) => set({ listen: value }),
|
setListen: (value: boolean) => set({ listen: value }),
|
||||||
setRecognitionLanguage: (language: string) => set({ recognitionLanguage: language }),
|
setRecognitionLanguage: (language: string) => set({ recognitionLanguage: language }),
|
||||||
setVolcengineAucAppId: (appId: string) => {
|
|
||||||
// 同时更新 zustand 状态和原有的 localStorage key
|
|
||||||
try {
|
|
||||||
localStorage.setItem('VOLCENGINE_AUC_APPID', appId);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to save VOLCENGINE_AUC_APPID to localStorage:', error);
|
|
||||||
}
|
|
||||||
set({ volcengineAucAppId: appId });
|
|
||||||
},
|
|
||||||
setVolcengineAucToken: (token: string) => {
|
|
||||||
// 同时更新 zustand 状态和原有的 localStorage key
|
|
||||||
try {
|
|
||||||
localStorage.setItem('VOLCENGINE_AUC_TOKEN', token);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to save VOLCENGINE_AUC_TOKEN to localStorage:', error);
|
|
||||||
}
|
|
||||||
set({ volcengineAucToken: token });
|
|
||||||
},
|
|
||||||
|
|
||||||
// 重置为默认设置
|
// 重置为默认设置
|
||||||
resetToDefault: () => {
|
resetToDefault: () => {
|
||||||
try {
|
|
||||||
localStorage.removeItem('VOLCENGINE_AUC_APPID');
|
|
||||||
localStorage.removeItem('VOLCENGINE_AUC_TOKEN');
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to remove volcengine config from localStorage:', error);
|
|
||||||
}
|
|
||||||
set({
|
set({
|
||||||
...defaultSettings,
|
...defaultSettings,
|
||||||
volcengineAucAppId: '',
|
|
||||||
volcengineAucToken: '',
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -112,22 +62,4 @@ export const useSettingStore = create<SettingState>()(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// 兼容原有 config.ts 的 API
|
|
||||||
export const getConfig = () => {
|
|
||||||
const state = useSettingStore.getState();
|
|
||||||
return {
|
|
||||||
VOLCENGINE_AUC_APPID: state.volcengineAucAppId,
|
|
||||||
VOLCENGINE_AUC_TOKEN: state.volcengineAucToken,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setConfig = (config: { VOLCENGINE_AUC_APPID?: string; VOLCENGINE_AUC_TOKEN?: string }) => {
|
|
||||||
const { setVolcengineAucAppId, setVolcengineAucToken } = useSettingStore.getState();
|
|
||||||
|
|
||||||
if (config.VOLCENGINE_AUC_APPID !== undefined) {
|
|
||||||
setVolcengineAucAppId(config.VOLCENGINE_AUC_APPID);
|
|
||||||
}
|
|
||||||
if (config.VOLCENGINE_AUC_TOKEN !== undefined) {
|
|
||||||
setVolcengineAucToken(config.VOLCENGINE_AUC_TOKEN);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,35 +1,49 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { devtools, persist } from 'zustand/middleware';
|
import { devtools } from 'zustand/middleware';
|
||||||
import { Speak, getDayOfYear, CreateSpeakData } from '../modules/speak-db/speak';
|
import { Speak, getDayOfYear } from '../modules/speak-db/speak';
|
||||||
import { speakService } from '../modules/speak-db/speak-service';
|
|
||||||
import { getText } from '../modules/text';
|
import { getText } from '../modules/text';
|
||||||
import { useSettingStore } from './settingStore';
|
import { useSettingStore } from './settingStore';
|
||||||
import { Relatime } from './relatime';
|
import { queryResources, useResourceStore } from './resourceStore';
|
||||||
|
import { queryApi as flowmeApi } from '@/modules/flowme-api';
|
||||||
|
import { queryApi as asrApi } from '@/modules/asr-api';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
export type FlowmeSpeak = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
tags: string[];
|
||||||
|
link: string;
|
||||||
|
type: string;
|
||||||
|
data: {
|
||||||
|
type: string;
|
||||||
|
speak: Speak;
|
||||||
|
}
|
||||||
|
}
|
||||||
interface VoiceState {
|
interface VoiceState {
|
||||||
// 状态数据
|
// 状态数据
|
||||||
voiceList: Speak[];
|
voiceList: FlowmeSpeak[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
currentDay: number;
|
currentDay: number;
|
||||||
|
|
||||||
|
// 删除确认弹窗
|
||||||
|
deleteConfirm: {
|
||||||
|
open: boolean;
|
||||||
|
voice: FlowmeSpeak | null;
|
||||||
|
};
|
||||||
|
|
||||||
// 动作方法
|
// 动作方法
|
||||||
initialize: () => Promise<void>;
|
initialize: () => Promise<void>;
|
||||||
|
getList: () => Promise<any>;
|
||||||
addVoice: (url: string, duration: number, audioBlob?: Blob) => Promise<Speak>;
|
addVoice: (url: string, duration: number, audioBlob?: Blob) => Promise<Speak>;
|
||||||
updateVoice: (id: string, updates: Partial<Speak>) => Promise<void>;
|
deleteVoice: (voice: FlowmeSpeak) => Promise<void>;
|
||||||
deleteVoice: (id: string) => Promise<void>;
|
recognizeVoice: (voice: FlowmeSpeak) => Promise<string>;
|
||||||
recognizeVoice: (id: string) => Promise<string>;
|
|
||||||
clearTodayVoices: () => Promise<void>;
|
|
||||||
generateAudioUrls: () => Promise<void>;
|
|
||||||
refreshList: () => Promise<void>;
|
|
||||||
setError: (error: string | null) => void;
|
setError: (error: string | null) => void;
|
||||||
setLoading: (loading: boolean) => void;
|
setLoading: (loading: boolean) => void;
|
||||||
relatime: Relatime;
|
showText: string;
|
||||||
relatimeParialText: string;
|
setShowText: (text: string) => void;
|
||||||
lastRecognizedText: string;
|
openDeleteConfirm: (voice: FlowmeSpeak) => void;
|
||||||
relatimeFinalText: string;
|
closeDeleteConfirm: () => void;
|
||||||
setRelatimeParialText: (text: string) => void;
|
|
||||||
setRelatimeFinalText: (text: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助函数:将 Blob 转换为 base64 字符串(兼容 Node.js)
|
// 辅助函数:将 Blob 转换为 base64 字符串(兼容 Node.js)
|
||||||
@@ -83,31 +97,29 @@ export const useVoiceStore = create<VoiceState>()(
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
currentDay: getDayOfYear(),
|
currentDay: getDayOfYear(),
|
||||||
|
deleteConfirm: {
|
||||||
// 初始化:从 IndexedDB 获取当天的记录
|
open: false,
|
||||||
initialize: async () => {
|
voice: null,
|
||||||
const { setLoading, setError, generateAudioUrls } = get();
|
},
|
||||||
const relatime = new Relatime();
|
getList: async () => {
|
||||||
set({ relatime });
|
const { setLoading, setError } = get();
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
// 初始化 speak service
|
|
||||||
await speakService.init();
|
|
||||||
|
|
||||||
// 获取当天的语音记录
|
// 获取当天的语音记录
|
||||||
const currentDay = getDayOfYear();
|
const currentDay = getDayOfYear();
|
||||||
const todayVoices = await speakService.getSpeaksByDay(currentDay);
|
const res = await flowmeApi.flowme.list({
|
||||||
|
type: 'muse',
|
||||||
set({
|
pageSize: 99999,
|
||||||
voiceList: todayVoices,
|
})
|
||||||
currentDay: currentDay
|
console.log('Flowme 列出资源结果:', res);
|
||||||
});
|
if (res.code === 200) {
|
||||||
|
set({
|
||||||
// 为获取到的记录生成 audio URLs
|
voiceList: res.data.list || [],
|
||||||
await generateAudioUrls();
|
currentDay: currentDay
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('初始化语音列表失败:', error);
|
console.error('初始化语音列表失败:', error);
|
||||||
setError(error instanceof Error ? error.message : '初始化失败');
|
setError(error instanceof Error ? error.message : '初始化失败');
|
||||||
@@ -115,10 +127,14 @@ export const useVoiceStore = create<VoiceState>()(
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// 初始化:从 IndexedDB 获取当天的记录
|
||||||
|
initialize: async () => {
|
||||||
|
await get().getList();
|
||||||
|
},
|
||||||
|
|
||||||
// 添加新的语音记录
|
// 添加新的语音记录
|
||||||
addVoice: async (url: string, duration: number, audioBlob?: Blob) => {
|
addVoice: async (url: string, duration: number, audioBlob?: Blob) => {
|
||||||
const { setError } = get();
|
const { setError, setShowText } = get();
|
||||||
const autoRecognize = useSettingStore.getState().autoRecognize;
|
const autoRecognize = useSettingStore.getState().autoRecognize;
|
||||||
try {
|
try {
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -133,7 +149,6 @@ export const useVoiceStore = create<VoiceState>()(
|
|||||||
// 创建语音记录(不保存 url,只保存 base64 数据)
|
// 创建语音记录(不保存 url,只保存 base64 数据)
|
||||||
const speakData = {
|
const speakData = {
|
||||||
duration: Math.ceil(duration),
|
duration: Math.ceil(duration),
|
||||||
file: fileData, // 保存 base64 数据而不是 url
|
|
||||||
day: getDayOfYear(),
|
day: getDayOfYear(),
|
||||||
no: 0, // 将由 service 自动生成
|
no: 0, // 将由 service 自动生成
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
@@ -141,23 +156,20 @@ export const useVoiceStore = create<VoiceState>()(
|
|||||||
text: '', // 初始为空
|
text: '', // 初始为空
|
||||||
};
|
};
|
||||||
if (autoRecognize) {
|
if (autoRecognize) {
|
||||||
speakData.text = await getText(fileData || '').then(res => res.text);
|
const res = await asrApi.asr.text({
|
||||||
|
base64Data: fileData || '',
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
const text = res.data.text || ''
|
||||||
|
speakData.text = text;
|
||||||
|
setShowText(text);
|
||||||
|
} else {
|
||||||
|
toast.error('语音识别失败: ' + res.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 保存到 IndexedDB(不包含 url)
|
const resourceStore = useResourceStore.getState();
|
||||||
const newSpeak = await speakService.createSpeakAuto(speakData);
|
resourceStore.upload(speakData as Speak, audioBlob);
|
||||||
|
await get().getList();
|
||||||
// 为新记录生成 URL 并添加到状态
|
|
||||||
const speakWithUrl = {
|
|
||||||
...newSpeak,
|
|
||||||
url: newSpeak.file ? base64ToUrl(newSpeak.file) : url
|
|
||||||
};
|
|
||||||
|
|
||||||
set(state => ({
|
|
||||||
voiceList: [...state.voiceList, speakWithUrl]
|
|
||||||
}));
|
|
||||||
|
|
||||||
return speakWithUrl;
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('添加语音记录失败:', error);
|
console.error('添加语音记录失败:', error);
|
||||||
setError(error instanceof Error ? error.message : '添加失败');
|
setError(error instanceof Error ? error.message : '添加失败');
|
||||||
@@ -165,63 +177,26 @@ export const useVoiceStore = create<VoiceState>()(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 更新语音记录
|
|
||||||
updateVoice: async (id: string, updates: Partial<Speak>) => {
|
|
||||||
const { setError } = get();
|
|
||||||
|
|
||||||
try {
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
// 从更新数据中移除 url,因为 url 不应该保存到 IndexedDB
|
|
||||||
const { url, ...updatesWithoutUrl } = updates;
|
|
||||||
|
|
||||||
// 更新 IndexedDB 中的记录
|
|
||||||
const updatedSpeak = await speakService.updateSpeak(id, updatesWithoutUrl);
|
|
||||||
|
|
||||||
// 更新状态中的记录
|
|
||||||
set(state => ({
|
|
||||||
voiceList: state.voiceList.map(voice => {
|
|
||||||
if (voice.id === id) {
|
|
||||||
const updated = { ...voice, ...updates };
|
|
||||||
// 如果更新了 file 数据,重新生成 URL
|
|
||||||
if (updatesWithoutUrl.file) {
|
|
||||||
updated.url = base64ToUrl(updatesWithoutUrl.file);
|
|
||||||
}
|
|
||||||
return updated;
|
|
||||||
}
|
|
||||||
return voice;
|
|
||||||
})
|
|
||||||
}));
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('更新语音记录失败:', error);
|
|
||||||
setError(error instanceof Error ? error.message : '更新失败');
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 删除语音记录
|
// 删除语音记录
|
||||||
deleteVoice: async (id: string) => {
|
deleteVoice: async (voice: FlowmeSpeak) => {
|
||||||
const { setError } = get();
|
const { setError } = get();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setError(null);
|
setError(null);
|
||||||
|
// TODO:
|
||||||
// 从 IndexedDB 删除
|
|
||||||
await speakService.deleteSpeak(id);
|
|
||||||
|
|
||||||
// 从状态中移除并释放 URL
|
// 从状态中移除并释放 URL
|
||||||
set(state => {
|
set(state => {
|
||||||
const voiceToDelete = state.voiceList.find(voice => voice.id === id);
|
|
||||||
if (voiceToDelete && voiceToDelete.url && voiceToDelete.url.startsWith('blob:')) {
|
|
||||||
URL.revokeObjectURL(voiceToDelete.url);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
voiceList: state.voiceList.filter(voice => voice.id !== id)
|
voiceList: state.voiceList.filter(v => v.id !== voice.id)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
const res = await flowmeApi.flowme.delete({ data: { id: voice.id } });
|
||||||
|
console.log('Flowme 删除资源结果:', res);
|
||||||
|
if (voice.link.startsWith('/')) {
|
||||||
|
const path = queryResources.getRelativePath(voice.link);
|
||||||
|
const deleteResult = await queryResources.deleteFile(path);
|
||||||
|
console.log('资源删除结果:', deleteResult);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('删除语音记录失败:', error);
|
console.error('删除语音记录失败:', error);
|
||||||
setError(error instanceof Error ? error.message : '删除失败');
|
setError(error instanceof Error ? error.message : '删除失败');
|
||||||
@@ -230,38 +205,32 @@ export const useVoiceStore = create<VoiceState>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 识别语音记录
|
// 识别语音记录
|
||||||
recognizeVoice: async (id: string) => {
|
recognizeVoice: async (voice: FlowmeSpeak) => {
|
||||||
const { setError } = get();
|
const { setError } = get();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setError(null);
|
setError(null);
|
||||||
|
const res = await asrApi.asr.link({
|
||||||
// 获取语音记录
|
url: voice.link,
|
||||||
const voice = get().voiceList.find(v => v.id === id);
|
})
|
||||||
if (!voice || !voice.file) {
|
if (res.code === 200) {
|
||||||
throw new Error('找不到语音记录或音频数据');
|
const text = res.data.text || ''
|
||||||
|
flowmeApi.flowme.update({
|
||||||
|
data: {
|
||||||
|
id: voice.id,
|
||||||
|
data: {
|
||||||
|
...voice.data,
|
||||||
|
speak: {
|
||||||
|
...voice.data.speak,
|
||||||
|
text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
get().getList();
|
||||||
|
} else {
|
||||||
|
toast.error('语音识别失败: ' + res.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用语音识别API
|
|
||||||
const result = await getText(voice.file);
|
|
||||||
const recognizedText = result.text;
|
|
||||||
|
|
||||||
if (!recognizedText) {
|
|
||||||
throw new Error('语音识别失败,未能获取到文字内容');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新数据库中的记录
|
|
||||||
await speakService.updateSpeak(id, { text: recognizedText });
|
|
||||||
|
|
||||||
// 更新状态中的记录
|
|
||||||
set(state => ({
|
|
||||||
voiceList: state.voiceList.map(voice =>
|
|
||||||
voice.id === id ? { ...voice, text: recognizedText } : voice
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
|
|
||||||
return recognizedText;
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('语音识别失败:', error);
|
console.error('语音识别失败:', error);
|
||||||
setError(error instanceof Error ? error.message : '语音识别失败');
|
setError(error instanceof Error ? error.message : '语音识别失败');
|
||||||
@@ -269,84 +238,27 @@ export const useVoiceStore = create<VoiceState>()(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 清空今天的语音记录
|
|
||||||
clearTodayVoices: async () => {
|
|
||||||
const { setError, currentDay } = get();
|
|
||||||
|
|
||||||
try {
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
// 从 IndexedDB 清空今天的记录
|
|
||||||
await speakService.deleteSpeaksByDay(currentDay);
|
|
||||||
|
|
||||||
// 清空状态并释放所有 URL
|
|
||||||
set(state => {
|
|
||||||
state.voiceList.forEach(voice => {
|
|
||||||
if (voice.url && voice.url.startsWith('blob:')) {
|
|
||||||
URL.revokeObjectURL(voice.url);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return { voiceList: [] };
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('清空今天语音记录失败:', error);
|
|
||||||
setError(error instanceof Error ? error.message : '清空失败');
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 为所有记录生成音频 URL
|
|
||||||
generateAudioUrls: async () => {
|
|
||||||
const { voiceList } = get();
|
|
||||||
|
|
||||||
set(state => ({
|
|
||||||
voiceList: state.voiceList.map(voice => {
|
|
||||||
// 如果已经有 URL 且是 blob URL,跳过
|
|
||||||
if (voice.url && voice.url.startsWith('blob:')) {
|
|
||||||
return voice;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果有 file 数据,从 base64 生成 URL
|
|
||||||
if (voice.file) {
|
|
||||||
return {
|
|
||||||
...voice,
|
|
||||||
url: base64ToUrl(voice.file)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return voice;
|
|
||||||
})
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
// 刷新列表(重新从 IndexedDB 获取)
|
|
||||||
refreshList: async () => {
|
|
||||||
const { initialize } = get();
|
|
||||||
await initialize();
|
|
||||||
},
|
|
||||||
|
|
||||||
// 设置错误信息
|
// 设置错误信息
|
||||||
setError: (error: string | null) => {
|
setError: (error: string | null) => {
|
||||||
set({ error });
|
set({ error });
|
||||||
},
|
},
|
||||||
|
|
||||||
// 设置加载状态
|
// 设置加载状态
|
||||||
setLoading: (loading: boolean) => {
|
setLoading: (loading: boolean) => {
|
||||||
set({ isLoading: loading });
|
set({ isLoading: loading });
|
||||||
},
|
},
|
||||||
|
// 设置显示的文本(识别结果)
|
||||||
relatimeFinalText: '',
|
showText: '',
|
||||||
setRelatimeFinalText: (text: string) => {
|
setShowText: (text: string) => {
|
||||||
const { relatimeFinalText } = get();
|
set({ showText: text });
|
||||||
set(() => ({ relatimeFinalText: text, relatimeParialText: '', lastRecognizedText: relatimeFinalText }));
|
|
||||||
},
|
},
|
||||||
setRelatimeParialText: (text: string) => {
|
// 打开删除确认弹窗
|
||||||
set({ relatimeParialText: text });
|
openDeleteConfirm: (voice: FlowmeSpeak) => {
|
||||||
|
set({ deleteConfirm: { open: true, voice } });
|
||||||
|
},
|
||||||
|
// 关闭删除确认弹窗
|
||||||
|
closeDeleteConfirm: () => {
|
||||||
|
set({ deleteConfirm: { open: false, voice: null } });
|
||||||
},
|
},
|
||||||
relatimeParialText: '',
|
|
||||||
lastRecognizedText: '',
|
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'voice-store', // persist key
|
name: 'voice-store', // persist key
|
||||||
@@ -356,23 +268,3 @@ export const useVoiceStore = create<VoiceState>()(
|
|||||||
|
|
||||||
// 导出类型以便其他地方使用
|
// 导出类型以便其他地方使用
|
||||||
export type { VoiceState };
|
export type { VoiceState };
|
||||||
|
|
||||||
// 辅助 hooks
|
|
||||||
export const useVoiceList = () => useVoiceStore(state => state.voiceList);
|
|
||||||
export const useVoiceLoading = () => useVoiceStore(state => state.isLoading);
|
|
||||||
export const useVoiceError = () => useVoiceStore(state => state.error);
|
|
||||||
|
|
||||||
// 清理函数:页面卸载时释放所有 blob URLs
|
|
||||||
export const cleanupVoiceUrls = () => {
|
|
||||||
const { voiceList } = useVoiceStore.getState();
|
|
||||||
voiceList.forEach(voice => {
|
|
||||||
if (voice.url && voice.url.startsWith('blob:')) {
|
|
||||||
URL.revokeObjectURL(voice.url);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 在页面卸载时自动清理
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.addEventListener('beforeunload', cleanupVoiceUrls);
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
import { Record } from './muse/index';
|
import { App } from './muse/voice/index';
|
||||||
|
|
||||||
export default Record;
|
export default App;
|
||||||
Reference in New Issue
Block a user