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:
2026-03-12 03:09:45 +08:00
parent 704303be13
commit 9b6dc45e90
15 changed files with 776 additions and 739 deletions

View File

@@ -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
View File

@@ -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
View 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
View 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 };

View File

@@ -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>
} }

View 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>
);
};

View File

@@ -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}

View File

@@ -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>

View File

@@ -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天中的第几天

View File

@@ -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';

View File

@@ -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);
}
}

View 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);
}
}
}));

View File

@@ -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);
}
};

View File

@@ -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);
}

View File

@@ -1,3 +1,3 @@
import { Record } from './muse/index'; import { App } from './muse/voice/index';
export default Record; export default App;