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",
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.2.0",
|
||||
"@kevisual/api": "^0.0.62",
|
||||
"@kevisual/api": "^0.0.63",
|
||||
"@kevisual/context": "^0.0.8",
|
||||
"@kevisual/router": "0.1.1",
|
||||
"@kevisual/video-tools": "^0.0.13",
|
||||
@@ -69,6 +69,6 @@
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"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
|
||||
version: 1.2.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@kevisual/api':
|
||||
specifier: ^0.0.62
|
||||
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))
|
||||
specifier: ^0.0.63
|
||||
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':
|
||||
specifier: ^0.0.8
|
||||
version: 0.0.8
|
||||
@@ -113,16 +113,16 @@ importers:
|
||||
version: 0.0.12
|
||||
'@kevisual/vite-html-plugin':
|
||||
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':
|
||||
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':
|
||||
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)
|
||||
'@tanstack/router-plugin':
|
||||
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':
|
||||
specifier: ^25.4.0
|
||||
version: 25.4.0
|
||||
@@ -134,7 +134,7 @@ importers:
|
||||
version: 19.2.3(@types/react@19.2.14)
|
||||
'@vitejs/plugin-react':
|
||||
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:
|
||||
specifier: ^17.3.1
|
||||
version: 17.3.1
|
||||
@@ -151,8 +151,8 @@ importers:
|
||||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: v8.0.0-beta.16
|
||||
version: 8.0.0-beta.16(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)
|
||||
specifier: v8.0.0-beta.18
|
||||
version: 8.0.0-beta.18(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -672,15 +672,12 @@ packages:
|
||||
'@kevisual/ai@0.0.28':
|
||||
resolution: {integrity: sha512-GLwCNXfopDvOj+hEZwEIwOV2/3VGd+TCPgBClaYuAv30KzhgehlCW05HPjBducSg+uPcdKacEzZsecHjo5fMUQ==}
|
||||
|
||||
'@kevisual/api@0.0.62':
|
||||
resolution: {integrity: sha512-GB8Ho2absXoXoZP2GKyuoRqRqjdwtV0JR512DXBaKJR2sIPn1KvuglbBiX+zPjDBBskv/ApvZKOoSwj1OmkrKQ==}
|
||||
'@kevisual/api@0.0.63':
|
||||
resolution: {integrity: sha512-juED4uDgHE9t6kfQRoktn6D1+LoA2bUBaoihTotVP1Cx2b14hBLZEbv0f/iNFzKfRwyYCwwTW6hRjjumOaFibQ==}
|
||||
|
||||
'@kevisual/context@0.0.8':
|
||||
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':
|
||||
resolution: {integrity: sha512-FcbOsmS1inhwrfgXMM/XLFTGTHUxBCss32JEMYdEFWQDYCar5rN8cxD1W8FuKDTVRlpA+zBpQ/BE6XT4UaeljA==}
|
||||
|
||||
@@ -1016,83 +1013,97 @@ packages:
|
||||
'@ricky0123/vad-web@0.0.30':
|
||||
resolution: {integrity: sha512-cJyYrh4YeeUBJcbR9Bic/bFDyB9qBkAepvpuWM3vLxnAi7bC3VHzf51UeNdT+OtY4D7MLAgV8iJMc4z41ZnaWg==}
|
||||
|
||||
'@rolldown/binding-android-arm64@1.0.0-rc.6':
|
||||
resolution: {integrity: sha512-kvjTSWGcrv+BaR2vge57rsKiYdVR8V8CoS0vgKrc570qRBfty4bT+1X0z3j2TaVV+kAYzA0PjeB9+mdZyqUZlg==}
|
||||
'@rolldown/binding-android-arm64@1.0.0-rc.8':
|
||||
resolution: {integrity: sha512-5bcmMQDWEfWUq3m79Mcf/kbO6e5Jr6YjKSsA1RnpXR6k73hQ9z1B17+4h93jXpzHvS18p7bQHM1HN/fSd+9zog==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@rolldown/binding-darwin-arm64@1.0.0-rc.6':
|
||||
resolution: {integrity: sha512-+tJhD21KvGNtUrpLXrZQlT+j5HZKiEwR2qtcZb3vNOUpvoT9QjEykr75ZW/Kr0W89gose/HVXU6351uVZD8Qvw==}
|
||||
'@rolldown/binding-darwin-arm64@1.0.0-rc.8':
|
||||
resolution: {integrity: sha512-dcHPd5N4g9w2iiPRJmAvO0fsIWzF2JPr9oSuTjxLL56qu+oML5aMbBMNwWbk58Mt3pc7vYs9CCScwLxdXPdRsg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@rolldown/binding-darwin-x64@1.0.0-rc.6':
|
||||
resolution: {integrity: sha512-DKNhjMk38FAWaHwUt1dFR3rA/qRAvn2NUvSG2UGvxvlMxSmN/qqww/j4ABAbXhNRXtGQNmrAINMXRuwHl16ZHg==}
|
||||
'@rolldown/binding-darwin-x64@1.0.0-rc.8':
|
||||
resolution: {integrity: sha512-mw0VzDvoj8AuR761QwpdCFN0sc/jspuc7eRYJetpLWd+XyansUrH3C7IgNw6swBOgQT9zBHNKsVCjzpfGJlhUA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@rolldown/binding-freebsd-x64@1.0.0-rc.6':
|
||||
resolution: {integrity: sha512-8TThsRkCPAnfyMBShxrGdtoOE6h36QepqRQI97iFaQSCRbHFWHcDHppcojZnzXoruuhPnjMEygzaykvPVJsMRg==}
|
||||
'@rolldown/binding-freebsd-x64@1.0.0-rc.8':
|
||||
resolution: {integrity: sha512-xNrRa6mQ9NmMIJBdJtPMPG8Mso0OhM526pDzc/EKnRrIrrkHD1E0Z6tONZRmUeJElfsQ6h44lQQCcDilSNIvSQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.6':
|
||||
resolution: {integrity: sha512-ZfmFoOwPUZCWtGOVC9/qbQzfc0249FrRUOzV2XabSMUV60Crp211OWLQN1zmQAsRIVWRcEwhJ46Z1mXGo/L/nQ==}
|
||||
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.8':
|
||||
resolution: {integrity: sha512-WgCKoO6O/rRUwimWfEJDeztwJJmuuX0N2bYLLRxmXDTtCwjToTOqk7Pashl/QpQn3H/jHjx0b5yCMbcTVYVpNg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.6':
|
||||
resolution: {integrity: sha512-ZsGzbNETxPodGlLTYHaCSGVhNN/rvkMDCJYHdT7PZr5jFJRmBfmDi2awhF64Dt2vxrJqY6VeeYSgOzEbHRsb7Q==}
|
||||
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.8':
|
||||
resolution: {integrity: sha512-tOHgTOQa8G4Z3ULj4G3NYOGGJEsqPHR91dT72u63OtVsZ7B6wFJKOx+ZKv+pvwzxWz92/I2ycaqi2/Ll4l+rlg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.6':
|
||||
resolution: {integrity: sha512-elPpdevtCdUOqziemR86C4CSCr/5sUxalzDrf/CJdMT+kZt2C556as++qHikNOz0vuFf52h+GJNXZM08eWgGPQ==}
|
||||
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.8':
|
||||
resolution: {integrity: sha512-oRbxcgDujCi2Yp1GTxoUFsIFlZsuPHU4OV4AzNc3/6aUmR4lfm9FK0uwQu82PJsuUwnF2jFdop3Ep5c1uK7Uxg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.6':
|
||||
resolution: {integrity: sha512-IBwXsf56o3xhzAyaZxdM1CX8UFiBEUFCjiVUgny67Q8vPIqkjzJj0YKhd3TbBHanuxThgBa59f6Pgutg2OGk5A==}
|
||||
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.8':
|
||||
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}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.0.0-rc.6':
|
||||
resolution: {integrity: sha512-vOk7G8V9Zm+8a6PL6JTpCea61q491oYlGtO6CvnsbhNLlKdf0bbCPytFzGQhYmCKZDKkEbmnkcIprTEGCURnwg==}
|
||||
'@rolldown/binding-linux-x64-musl@1.0.0-rc.8':
|
||||
resolution: {integrity: sha512-bGyXCFU11seFrf7z8PcHSwGEiFVkZ9vs+auLacVOQrVsI8PFHJzzJROF3P6b0ODDmXr0m6Tj5FlDhcXVk0Jp8w==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.0.0-rc.6':
|
||||
resolution: {integrity: sha512-ASjEDI4MRv7XCQb2JVaBzfEYO98JKCGrAgoW6M03fJzH/ilCnC43Mb3ptB9q/lzsaahoJyIBoAGKAYEjUvpyvQ==}
|
||||
'@rolldown/binding-openharmony-arm64@1.0.0-rc.8':
|
||||
resolution: {integrity: sha512-n8d+L2bKgf9G3+AM0bhHFWdlz9vYKNim39ujRTieukdRek0RAo2TfG2uEnV9spa4r4oHUfL9IjcY3M9SlqN1gw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@rolldown/binding-wasm32-wasi@1.0.0-rc.6':
|
||||
resolution: {integrity: sha512-mYa1+h2l6Zc0LvmwUh0oXKKYihnw/1WC73vTqw+IgtfEtv47A+rWzzcWwVDkW73+UDr0d/Ie/HRXoaOY22pQDw==}
|
||||
'@rolldown/binding-wasm32-wasi@1.0.0-rc.8':
|
||||
resolution: {integrity: sha512-4R4iJDIk7BrJdteAbEAICXPoA7vZoY/M0OBfcRlQxzQvUYMcEp2GbC/C8UOgQJhu2TjGTpX1H8vVO1xHWcRqQA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.6':
|
||||
resolution: {integrity: sha512-e2ABskbNH3MRUBMjgxaMjYIw11DSwjLJxBII3UgpF6WClGLIh8A20kamc+FKH5vIaFVnYQInmcLYSUVpqMPLow==}
|
||||
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.8':
|
||||
resolution: {integrity: sha512-3lwnklba9qQOpFnQ7EW+A1m4bZTWXZE4jtehsZ0YOl2ivW1FQqp5gY7X2DLuKITggesyuLwcmqS11fA7NtrmrA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.6':
|
||||
resolution: {integrity: sha512-dJVc3ifhaRXxIEh1xowLohzFrlQXkJ66LepHm+CmSprTWgVrPa8Fx3OL57xwIqDEH9hufcKkDX2v65rS3NZyRA==}
|
||||
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.8':
|
||||
resolution: {integrity: sha512-VGjCx9Ha1P/r3tXGDZyG0Fcq7Q0Afnk64aaKzr1m40vbn1FL8R3W0V1ELDvPgzLXaaqK/9PnsqSaLWXfn6JtGQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -1100,8 +1111,8 @@ packages:
|
||||
'@rolldown/pluginutils@1.0.0-rc.3':
|
||||
resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==}
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-rc.6':
|
||||
resolution: {integrity: sha512-Y0+JT8Mi1mmW08K6HieG315XNRu4L0rkfCpA364HtytjgiqYnMYRdFPcxRl+BQQqNXzecL2S9nii+RUpO93XIA==}
|
||||
'@rolldown/pluginutils@1.0.0-rc.8':
|
||||
resolution: {integrity: sha512-wzJwL82/arVfeSP3BLr1oTy40XddjtEdrdgtJ4lLRBu06mP3q/8HGM6K0JRlQuTA3XB0pNJx2so/nmpY4xyOew==}
|
||||
|
||||
'@standard-schema/spec@1.1.0':
|
||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||
@@ -1940,8 +1951,8 @@ packages:
|
||||
resolve-pkg-maps@1.0.0:
|
||||
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||
|
||||
rolldown@1.0.0-rc.6:
|
||||
resolution: {integrity: sha512-B8vFPV1ADyegoYfhg+E7RAucYKv0xdVlwYYsIJgfPNeiSxZGWNxts9RqhyGzC11ULK/VaeXyKezGCwpMiH8Ktw==}
|
||||
rolldown@1.0.0-rc.8:
|
||||
resolution: {integrity: sha512-RGOL7mz/aoQpy/y+/XS9iePBfeNRDUdozrhCEJxdpJyimW8v6yp4c30q6OviUU5AnUJVLRL9GP//HUs6N3ALrQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
|
||||
@@ -2097,8 +2108,8 @@ packages:
|
||||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||
hasBin: true
|
||||
|
||||
vite@8.0.0-beta.16:
|
||||
resolution: {integrity: sha512-c0t7hYkxsjws89HH+BUFh/sL3BpPNhNsL9CJrTpMxBmwKQBRSa5OJ5w4o9O0bQVI/H/vx7UpUUIevvXa37NS/Q==}
|
||||
vite@8.0.0-beta.18:
|
||||
resolution: {integrity: sha512-azgNbWdsO/WBqHQxwSCy+zd+Fq+37Fix2hn64cQuiUvaaGGSUac7f8RGQhI1aQl9OKbfWblrCFLWs+tln06c2A==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -2601,10 +2612,10 @@ snapshots:
|
||||
ai: 6.0.116(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:
|
||||
'@kevisual/context': 0.0.8
|
||||
'@kevisual/js-filter': 0.0.5
|
||||
'@kevisual/js-filter': 0.0.6
|
||||
'@kevisual/load': 0.0.6
|
||||
'@paralleldrive/cuid2': 3.3.0
|
||||
es-toolkit: 1.45.1
|
||||
@@ -2624,8 +2635,6 @@ snapshots:
|
||||
|
||||
'@kevisual/context@0.0.8': {}
|
||||
|
||||
'@kevisual/js-filter@0.0.5': {}
|
||||
|
||||
'@kevisual/js-filter@0.0.6': {}
|
||||
|
||||
'@kevisual/kv-login@0.1.17': {}
|
||||
@@ -2680,9 +2689,9 @@ snapshots:
|
||||
|
||||
'@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:
|
||||
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':
|
||||
dependencies:
|
||||
@@ -2983,50 +2992,56 @@ snapshots:
|
||||
dependencies:
|
||||
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
|
||||
|
||||
'@rolldown/binding-darwin-arm64@1.0.0-rc.6':
|
||||
'@rolldown/binding-darwin-arm64@1.0.0-rc.8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-darwin-x64@1.0.0-rc.6':
|
||||
'@rolldown/binding-darwin-x64@1.0.0-rc.8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-freebsd-x64@1.0.0-rc.6':
|
||||
'@rolldown/binding-freebsd-x64@1.0.0-rc.8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.6':
|
||||
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.6':
|
||||
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.6':
|
||||
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.6':
|
||||
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.0.0-rc.6':
|
||||
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.0.0-rc.6':
|
||||
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.8':
|
||||
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:
|
||||
'@napi-rs/wasm-runtime': 1.1.1
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.6':
|
||||
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.6':
|
||||
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.8':
|
||||
optional: true
|
||||
|
||||
'@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': {}
|
||||
|
||||
@@ -3097,12 +3112,12 @@ snapshots:
|
||||
'@tailwindcss/oxide-win32-arm64-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:
|
||||
'@tailwindcss/node': 4.2.1
|
||||
'@tailwindcss/oxide': 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': {}
|
||||
|
||||
@@ -3174,7 +3189,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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:
|
||||
'@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
|
||||
optionalDependencies:
|
||||
'@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:
|
||||
- supports-color
|
||||
|
||||
@@ -3253,7 +3268,7 @@ snapshots:
|
||||
|
||||
'@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:
|
||||
'@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
|
||||
'@types/babel__core': 7.20.5
|
||||
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:
|
||||
- supports-color
|
||||
|
||||
@@ -3826,24 +3841,26 @@ snapshots:
|
||||
|
||||
resolve-pkg-maps@1.0.0: {}
|
||||
|
||||
rolldown@1.0.0-rc.6:
|
||||
rolldown@1.0.0-rc.8:
|
||||
dependencies:
|
||||
'@oxc-project/types': 0.115.0
|
||||
'@rolldown/pluginutils': 1.0.0-rc.6
|
||||
'@rolldown/pluginutils': 1.0.0-rc.8
|
||||
optionalDependencies:
|
||||
'@rolldown/binding-android-arm64': 1.0.0-rc.6
|
||||
'@rolldown/binding-darwin-arm64': 1.0.0-rc.6
|
||||
'@rolldown/binding-darwin-x64': 1.0.0-rc.6
|
||||
'@rolldown/binding-freebsd-x64': 1.0.0-rc.6
|
||||
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.6
|
||||
'@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.6
|
||||
'@rolldown/binding-linux-arm64-musl': 1.0.0-rc.6
|
||||
'@rolldown/binding-linux-x64-gnu': 1.0.0-rc.6
|
||||
'@rolldown/binding-linux-x64-musl': 1.0.0-rc.6
|
||||
'@rolldown/binding-openharmony-arm64': 1.0.0-rc.6
|
||||
'@rolldown/binding-wasm32-wasi': 1.0.0-rc.6
|
||||
'@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.6
|
||||
'@rolldown/binding-win32-x64-msvc': 1.0.0-rc.6
|
||||
'@rolldown/binding-android-arm64': 1.0.0-rc.8
|
||||
'@rolldown/binding-darwin-arm64': 1.0.0-rc.8
|
||||
'@rolldown/binding-darwin-x64': 1.0.0-rc.8
|
||||
'@rolldown/binding-freebsd-x64': 1.0.0-rc.8
|
||||
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.8
|
||||
'@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.8
|
||||
'@rolldown/binding-linux-arm64-musl': 1.0.0-rc.8
|
||||
'@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.8
|
||||
'@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.8
|
||||
'@rolldown/binding-linux-x64-gnu': 1.0.0-rc.8
|
||||
'@rolldown/binding-linux-x64-musl': 1.0.0-rc.8
|
||||
'@rolldown/binding-openharmony-arm64': 1.0.0-rc.8
|
||||
'@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: {}
|
||||
|
||||
@@ -3978,13 +3995,13 @@ snapshots:
|
||||
|
||||
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:
|
||||
'@oxc-project/runtime': 0.115.0
|
||||
lightningcss: 1.31.1
|
||||
picomatch: 4.0.3
|
||||
postcss: 8.5.6
|
||||
rolldown: 1.0.0-rc.6
|
||||
rolldown: 1.0.0-rc.8
|
||||
tinyglobby: 0.2.15
|
||||
optionalDependencies:
|
||||
'@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 { ConfirmDeleteModal } from './modules/ConfirmDeleteModal';
|
||||
|
||||
|
||||
export const App = () => {
|
||||
return <div className="h-full overflow-hidden">
|
||||
<VadVoice />
|
||||
<ConfirmDeleteModal />
|
||||
</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,
|
||||
} from "../../../../components/ui/dialog";
|
||||
import { useSettingStore } from '../store/settingStore';
|
||||
import { PasswordInput } from '../../components/PasswordInput';
|
||||
import { X, RotateCcw } from 'lucide-react';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
export const SettingModal: React.FC = () => {
|
||||
const {
|
||||
isOpen,
|
||||
autoRecognize,
|
||||
listen,
|
||||
volcengineAucAppId,
|
||||
volcengineAucToken,
|
||||
closeModal,
|
||||
setAutoRecognize,
|
||||
setListen,
|
||||
setVolcengineAucAppId,
|
||||
setVolcengineAucToken,
|
||||
resetToDefault,
|
||||
} = useSettingStore();
|
||||
|
||||
@@ -104,36 +99,7 @@ export const SettingModal: React.FC = () => {
|
||||
</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 className="flex justify-end space-x-3 pt-4 border-t">
|
||||
<button
|
||||
onClick={handleClose}
|
||||
|
||||
@@ -2,40 +2,30 @@ import { MicVAD, utils } from "@ricky0123/vad-web"
|
||||
import clsx from "clsx";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
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 '@szhsin/react-menu/dist/index.css';
|
||||
import { toast } from 'sonner';
|
||||
import { Speak } from "./speak-db/speak";
|
||||
import { useVoiceStore } from "../store/voiceStore";
|
||||
import { FlowmeSpeak, useVoiceStore } from "../store/voiceStore";
|
||||
import { useSettingStore } from "../store/settingStore";
|
||||
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 { useResourceStore } from "../store/resourceStore";
|
||||
import { useShallow } from "zustand/shallow";
|
||||
import { useLayoutStore } from "@/pages/auth/store";
|
||||
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 [audio, setAudio] = useState<HTMLAudioElement | null>(null);
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data.url) return;
|
||||
const url = item.link;
|
||||
|
||||
const audioInstance = new Audio(data.url);
|
||||
const audioInstance = new Audio(url);
|
||||
audioRef.current = audioInstance;
|
||||
setAudio(audioInstance);
|
||||
|
||||
@@ -51,7 +41,7 @@ const VoicePlayer = ({ data }: VadVoiceProps) => {
|
||||
audioInstance.pause();
|
||||
audioInstance.src = '';
|
||||
};
|
||||
}, [data.url]);
|
||||
}, [item]);
|
||||
|
||||
const handlePlay = () => {
|
||||
if (audio) {
|
||||
@@ -76,27 +66,15 @@ const VoicePlayer = ({ data }: VadVoiceProps) => {
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
if (!data.url) return;
|
||||
if (!item.link) return;
|
||||
const link = document.createElement('a');
|
||||
link.href = data.url;
|
||||
link.href = item.link;
|
||||
link.download = `recording-${data.timestamp}.wav`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
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 = () => {
|
||||
if (data.text && data.text.trim()) {
|
||||
toast.info('该语音记录已经识别过了,文字内容:' + data.text);
|
||||
@@ -104,22 +82,14 @@ const VoicePlayer = ({ data }: VadVoiceProps) => {
|
||||
}
|
||||
|
||||
const { recognizeVoice } = useVoiceStore.getState();
|
||||
recognizeVoice(data.id)
|
||||
.then((text) => {
|
||||
console.log('语音识别成功:', text);
|
||||
toast.success('识别成功!文字内容:' + text);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('语音识别失败:', error);
|
||||
toast.error('识别失败: ' + (error instanceof Error ? error.message : '未知错误'));
|
||||
});
|
||||
recognizeVoice(item)
|
||||
};
|
||||
|
||||
return (
|
||||
<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">
|
||||
<MoreHorizontal className="w-4 h-4" />
|
||||
</MenuButton>
|
||||
@@ -150,51 +120,14 @@ const VoicePlayer = ({ data }: VadVoiceProps) => {
|
||||
<span>识别语音</span>
|
||||
</div>
|
||||
</MenuItem>
|
||||
{data.text && data.text.trim() ? (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<MenuItem>
|
||||
<MenuItem onClick={() => useVoiceStore.getState().openDeleteConfirm(item)}>
|
||||
<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"
|
||||
/>
|
||||
<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>
|
||||
</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>
|
||||
{/* 播放/暂停按钮 */}
|
||||
{!isPlaying ? (
|
||||
@@ -223,7 +156,69 @@ const VoicePlayer = ({ data }: VadVoiceProps) => {
|
||||
</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(() => {
|
||||
const bottomElement = document.getElementById('voice-list-bottom');
|
||||
if (bottomElement) {
|
||||
@@ -231,124 +226,8 @@ export const ShowVoicePlayer = ({ data }: { data: Speak[] }) => {
|
||||
}
|
||||
}, [data]);
|
||||
return (<ul className="space-y-2 max-h-full">
|
||||
{data.map((item, index) => (
|
||||
<li key={index} 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 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>
|
||||
{data.map((item) => (
|
||||
<ShowVoicePlayerItem key={item.id} item={item} />
|
||||
))}
|
||||
|
||||
<div id="voice-list-bottom" />
|
||||
@@ -362,12 +241,9 @@ export const VadVoice = () => {
|
||||
error: storeError,
|
||||
initialize: initializeStore,
|
||||
addVoice,
|
||||
showText,
|
||||
setError: setStoreError,
|
||||
relatimeParialText,
|
||||
relatimeFinalText,
|
||||
lastRecognizedText
|
||||
} = useVoiceStore();
|
||||
const showText = relatimeFinalText || relatimeParialText;
|
||||
// 使用设置 store
|
||||
const {
|
||||
openModal: openSettingModal,
|
||||
@@ -376,13 +252,14 @@ export const VadVoice = () => {
|
||||
autoRecognize,
|
||||
setAutoRecognize
|
||||
} = 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 [realListen, setRealListen] = useState<boolean>(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||
const [userInteracted, setUserInteracted] = useState<boolean>(false);
|
||||
const ref = useRef<MicVAD | null>(null);
|
||||
const audioRecorderRef = useRef<AudioRecorder | null>(null);
|
||||
const initializingRef = useRef<boolean>(false);
|
||||
|
||||
async function initializeVAD(ls: boolean = true) {
|
||||
@@ -399,26 +276,9 @@ export const VadVoice = () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
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) => {
|
||||
try {
|
||||
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 tempUrl = URL.createObjectURL(audioBlob)
|
||||
|
||||
@@ -444,13 +304,11 @@ export const VadVoice = () => {
|
||||
setRealListen(false);
|
||||
}
|
||||
},
|
||||
onnxWASMBasePath: "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.22.0/dist/",
|
||||
baseAssetPath: "https://cdn.jsdelivr.net/npm/@ricky0123/vad-web@0.0.27/dist/",
|
||||
onnxWASMBasePath: "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.24.3/dist/",
|
||||
baseAssetPath: "https://cdn.jsdelivr.net/npm/@ricky0123/vad-web@0.0.30/dist/",
|
||||
onSpeechRealStart: () => {
|
||||
console.log('VAD real start');
|
||||
setRealListen(true);
|
||||
const relatime = useVoiceStore.getState().relatime;
|
||||
relatime?.setStartTime?.(Date.now());
|
||||
},
|
||||
});
|
||||
|
||||
@@ -483,6 +341,10 @@ export const VadVoice = () => {
|
||||
useEffect(() => {
|
||||
initializeStore();
|
||||
}, [initializeStore]);
|
||||
useEffect(() => {
|
||||
if (!layoutStore.me?.id) return;
|
||||
resourceStore.init(layoutStore.me.username!);
|
||||
}, [layoutStore.me])
|
||||
useEffect(() => {
|
||||
// 页面加载时不自动初始化,等待用户交互
|
||||
const handleFirstClick = () => {
|
||||
@@ -521,8 +383,6 @@ export const VadVoice = () => {
|
||||
ref.current = null;
|
||||
setListen(false);
|
||||
setVadStatus('idle');
|
||||
audioRecorderRef.current?.stop();
|
||||
audioRecorderRef.current = null;
|
||||
console.log('VAD closed');
|
||||
}
|
||||
}
|
||||
@@ -604,8 +464,7 @@ export const VadVoice = () => {
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full">
|
||||
{lastRecognizedText && <ShowText text={lastRecognizedText} title="上次识别" icon={'🕘'} />}
|
||||
<ShowText text={showText} icon={'📝'} />
|
||||
{showText && <ShowText text={showText} icon={'📝'} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { nanoid } from "nanoid";
|
||||
export type Speak = {
|
||||
id: string;
|
||||
no: number; // 序号, 当天的序号
|
||||
file?: string; // base64 编码的音频文件
|
||||
text?: string; // 文字内容,识别的内容
|
||||
timestamp: number; // 生成时间戳
|
||||
day: number; // 365天中的第几天
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// Store exports
|
||||
export { useVoiceStore, useVoiceList, useVoiceLoading, useVoiceError, cleanupVoiceUrls } from './voiceStore';
|
||||
export { useVoiceStore } 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;
|
||||
recognitionLanguage: string;
|
||||
|
||||
// 火山引擎配置
|
||||
volcengineAucAppId: string;
|
||||
volcengineAucToken: string;
|
||||
|
||||
// 操作方法
|
||||
openModal: () => void;
|
||||
closeModal: () => void;
|
||||
setAutoRecognize: (value: boolean) => void;
|
||||
setListen: (value: boolean) => void;
|
||||
setRecognitionLanguage: (language: string) => void;
|
||||
setVolcengineAucAppId: (appId: string) => void;
|
||||
setVolcengineAucToken: (token: string) => void;
|
||||
resetToDefault: () => void;
|
||||
}
|
||||
|
||||
@@ -29,24 +23,6 @@ const defaultSettings = {
|
||||
autoRecognize: false,
|
||||
listen: false,
|
||||
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>()(
|
||||
@@ -55,7 +31,6 @@ export const useSettingStore = create<SettingState>()(
|
||||
// 初始状态 - 合并默认设置和从 localStorage 读取的火山引擎配置
|
||||
isOpen: false,
|
||||
...defaultSettings,
|
||||
...getInitialVolcengineConfig(),
|
||||
mount: false,
|
||||
// 弹窗控制方法
|
||||
openModal: () => set({ isOpen: true }),
|
||||
@@ -65,37 +40,12 @@ export const useSettingStore = create<SettingState>()(
|
||||
setAutoRecognize: (value: boolean) => set({ autoRecognize: value }),
|
||||
setListen: (value: boolean) => set({ listen: value }),
|
||||
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: () => {
|
||||
try {
|
||||
localStorage.removeItem('VOLCENGINE_AUC_APPID');
|
||||
localStorage.removeItem('VOLCENGINE_AUC_TOKEN');
|
||||
} catch (error) {
|
||||
console.warn('Failed to remove volcengine config from localStorage:', error);
|
||||
}
|
||||
set({
|
||||
...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 { devtools, persist } from 'zustand/middleware';
|
||||
import { Speak, getDayOfYear, CreateSpeakData } from '../modules/speak-db/speak';
|
||||
import { speakService } from '../modules/speak-db/speak-service';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import { Speak, getDayOfYear } from '../modules/speak-db/speak';
|
||||
import { getText } from '../modules/text';
|
||||
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 {
|
||||
// 状态数据
|
||||
voiceList: Speak[];
|
||||
voiceList: FlowmeSpeak[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
currentDay: number;
|
||||
|
||||
// 删除确认弹窗
|
||||
deleteConfirm: {
|
||||
open: boolean;
|
||||
voice: FlowmeSpeak | null;
|
||||
};
|
||||
|
||||
// 动作方法
|
||||
initialize: () => Promise<void>;
|
||||
getList: () => Promise<any>;
|
||||
addVoice: (url: string, duration: number, audioBlob?: Blob) => Promise<Speak>;
|
||||
updateVoice: (id: string, updates: Partial<Speak>) => Promise<void>;
|
||||
deleteVoice: (id: string) => Promise<void>;
|
||||
recognizeVoice: (id: string) => Promise<string>;
|
||||
clearTodayVoices: () => Promise<void>;
|
||||
generateAudioUrls: () => Promise<void>;
|
||||
refreshList: () => Promise<void>;
|
||||
deleteVoice: (voice: FlowmeSpeak) => Promise<void>;
|
||||
recognizeVoice: (voice: FlowmeSpeak) => Promise<string>;
|
||||
setError: (error: string | null) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
relatime: Relatime;
|
||||
relatimeParialText: string;
|
||||
lastRecognizedText: string;
|
||||
relatimeFinalText: string;
|
||||
setRelatimeParialText: (text: string) => void;
|
||||
setRelatimeFinalText: (text: string) => void;
|
||||
showText: string;
|
||||
setShowText: (text: string) => void;
|
||||
openDeleteConfirm: (voice: FlowmeSpeak) => void;
|
||||
closeDeleteConfirm: () => void;
|
||||
}
|
||||
|
||||
// 辅助函数:将 Blob 转换为 base64 字符串(兼容 Node.js)
|
||||
@@ -83,31 +97,29 @@ export const useVoiceStore = create<VoiceState>()(
|
||||
isLoading: false,
|
||||
error: null,
|
||||
currentDay: getDayOfYear(),
|
||||
|
||||
// 初始化:从 IndexedDB 获取当天的记录
|
||||
initialize: async () => {
|
||||
const { setLoading, setError, generateAudioUrls } = get();
|
||||
const relatime = new Relatime();
|
||||
set({ relatime });
|
||||
deleteConfirm: {
|
||||
open: false,
|
||||
voice: null,
|
||||
},
|
||||
getList: async () => {
|
||||
const { setLoading, setError } = get();
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// 初始化 speak service
|
||||
await speakService.init();
|
||||
|
||||
// 获取当天的语音记录
|
||||
const currentDay = getDayOfYear();
|
||||
const todayVoices = await speakService.getSpeaksByDay(currentDay);
|
||||
|
||||
const res = await flowmeApi.flowme.list({
|
||||
type: 'muse',
|
||||
pageSize: 99999,
|
||||
})
|
||||
console.log('Flowme 列出资源结果:', res);
|
||||
if (res.code === 200) {
|
||||
set({
|
||||
voiceList: todayVoices,
|
||||
voiceList: res.data.list || [],
|
||||
currentDay: currentDay
|
||||
});
|
||||
|
||||
// 为获取到的记录生成 audio URLs
|
||||
await generateAudioUrls();
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('初始化语音列表失败:', error);
|
||||
setError(error instanceof Error ? error.message : '初始化失败');
|
||||
@@ -115,10 +127,14 @@ export const useVoiceStore = create<VoiceState>()(
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
// 初始化:从 IndexedDB 获取当天的记录
|
||||
initialize: async () => {
|
||||
await get().getList();
|
||||
},
|
||||
|
||||
// 添加新的语音记录
|
||||
addVoice: async (url: string, duration: number, audioBlob?: Blob) => {
|
||||
const { setError } = get();
|
||||
const { setError, setShowText } = get();
|
||||
const autoRecognize = useSettingStore.getState().autoRecognize;
|
||||
try {
|
||||
setError(null);
|
||||
@@ -133,7 +149,6 @@ export const useVoiceStore = create<VoiceState>()(
|
||||
// 创建语音记录(不保存 url,只保存 base64 数据)
|
||||
const speakData = {
|
||||
duration: Math.ceil(duration),
|
||||
file: fileData, // 保存 base64 数据而不是 url
|
||||
day: getDayOfYear(),
|
||||
no: 0, // 将由 service 自动生成
|
||||
timestamp: Date.now(),
|
||||
@@ -141,23 +156,20 @@ export const useVoiceStore = create<VoiceState>()(
|
||||
text: '', // 初始为空
|
||||
};
|
||||
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 newSpeak = await speakService.createSpeakAuto(speakData);
|
||||
|
||||
// 为新记录生成 URL 并添加到状态
|
||||
const speakWithUrl = {
|
||||
...newSpeak,
|
||||
url: newSpeak.file ? base64ToUrl(newSpeak.file) : url
|
||||
};
|
||||
|
||||
set(state => ({
|
||||
voiceList: [...state.voiceList, speakWithUrl]
|
||||
}));
|
||||
|
||||
return speakWithUrl;
|
||||
|
||||
}
|
||||
const resourceStore = useResourceStore.getState();
|
||||
resourceStore.upload(speakData as Speak, audioBlob);
|
||||
await get().getList();
|
||||
} catch (error) {
|
||||
console.error('添加语音记录失败:', error);
|
||||
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();
|
||||
|
||||
try {
|
||||
setError(null);
|
||||
|
||||
// 从 IndexedDB 删除
|
||||
await speakService.deleteSpeak(id);
|
||||
|
||||
// TODO:
|
||||
// 从状态中移除并释放 URL
|
||||
set(state => {
|
||||
const voiceToDelete = state.voiceList.find(voice => voice.id === id);
|
||||
if (voiceToDelete && voiceToDelete.url && voiceToDelete.url.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(voiceToDelete.url);
|
||||
}
|
||||
|
||||
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) {
|
||||
console.error('删除语音记录失败:', error);
|
||||
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();
|
||||
|
||||
try {
|
||||
setError(null);
|
||||
|
||||
// 获取语音记录
|
||||
const voice = get().voiceList.find(v => v.id === id);
|
||||
if (!voice || !voice.file) {
|
||||
throw new Error('找不到语音记录或音频数据');
|
||||
const res = await asrApi.asr.link({
|
||||
url: voice.link,
|
||||
})
|
||||
if (res.code === 200) {
|
||||
const text = res.data.text || ''
|
||||
flowmeApi.flowme.update({
|
||||
data: {
|
||||
id: voice.id,
|
||||
data: {
|
||||
...voice.data,
|
||||
speak: {
|
||||
...voice.data.speak,
|
||||
text,
|
||||
}
|
||||
|
||||
// 调用语音识别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;
|
||||
|
||||
}
|
||||
})
|
||||
get().getList();
|
||||
} else {
|
||||
toast.error('语音识别失败: ' + res.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('语音识别失败:', error);
|
||||
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) => {
|
||||
set({ error });
|
||||
},
|
||||
|
||||
// 设置加载状态
|
||||
setLoading: (loading: boolean) => {
|
||||
set({ isLoading: loading });
|
||||
},
|
||||
|
||||
relatimeFinalText: '',
|
||||
setRelatimeFinalText: (text: string) => {
|
||||
const { relatimeFinalText } = get();
|
||||
set(() => ({ relatimeFinalText: text, relatimeParialText: '', lastRecognizedText: relatimeFinalText }));
|
||||
// 设置显示的文本(识别结果)
|
||||
showText: '',
|
||||
setShowText: (text: string) => {
|
||||
set({ showText: text });
|
||||
},
|
||||
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
|
||||
@@ -356,23 +268,3 @@ export const useVoiceStore = create<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