diff --git a/package.json b/package.json
index 0235f9a..61bd6fc 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
}
\ No newline at end of file
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index fc2776a..9fdcf90 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -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
diff --git a/src/modules/asr-api.ts b/src/modules/asr-api.ts
new file mode 100644
index 0000000..5a8d195
--- /dev/null
+++ b/src/modules/asr-api.ts
@@ -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 };
diff --git a/src/modules/flowme-api.ts b/src/modules/flowme-api.ts
new file mode 100644
index 0000000..15952f6
--- /dev/null
+++ b/src/modules/flowme-api.ts
@@ -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 };
diff --git a/src/pages/muse/voice/index.tsx b/src/pages/muse/voice/index.tsx
index 73a033d..be7600f 100644
--- a/src/pages/muse/voice/index.tsx
+++ b/src/pages/muse/voice/index.tsx
@@ -1,8 +1,10 @@
import { VadVoice } from './modules/VadVoice';
+import { ConfirmDeleteModal } from './modules/ConfirmDeleteModal';
export const App = () => {
return
+
}
\ No newline at end of file
diff --git a/src/pages/muse/voice/modules/ConfirmDeleteModal.tsx b/src/pages/muse/voice/modules/ConfirmDeleteModal.tsx
new file mode 100644
index 0000000..bdf9b38
--- /dev/null
+++ b/src/pages/muse/voice/modules/ConfirmDeleteModal.tsx
@@ -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 (
+
+ );
+};
diff --git a/src/pages/muse/voice/modules/SettingModal.tsx b/src/pages/muse/voice/modules/SettingModal.tsx
index d07aeee..c74737e 100644
--- a/src/pages/muse/voice/modules/SettingModal.tsx
+++ b/src/pages/muse/voice/modules/SettingModal.tsx
@@ -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();
@@ -58,7 +53,7 @@ export const SettingModal: React.FC = () => {
{/* 语音识别设置 */}
-
+
@@ -104,36 +99,7 @@ export const SettingModal: React.FC = () => {
-
- {/* 火山引擎配置 */}
-
-
火山引擎配置
-
-
-
-
- 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"
- />
-
-
-
-
-
-
);
}
-export const ShowVoicePlayer = ({ data }: { data: Speak[] }) => {
+const ShowVoicePlayerItem = ({ item }: { item: FlowmeSpeak }) => {
+ const speak = item.data.speak || {};
+ return
+
+
+
+
+
+
+
+
+
+ {speak ? new Date(speak.timestamp).toLocaleTimeString() : ''}
+
+
+ #{speak?.no}
+
+
+
+
{
+ 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="删除"
+ >
+
+
+
+
+ {speak.text && (
+
{
+ e.stopPropagation();
+ if (!speak.text) return;
+ try {
+ await navigator.clipboard.writeText(speak.text);
+ toast.success('文字已复制到剪贴板');
+ } catch (error) {
+ console.error('复制失败:', error);
+ toast.error('复制失败,请手动选择文字复制');
+ }
+ }}
+ >
+ 📝 {speak.text}
+
+ )}
+
+
+
+
+}
+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 (
- {data.map((item, index) => (
- -
-
-
-
-
-
-
-
-
-
- {new Date(item.timestamp).toLocaleTimeString()}
-
-
- #{item.no}
-
-
-
- {item.text && item.text.trim() ? (
-
-
-
-
-
-
- 确认删除
-
- 此语音包含文字内容:"{item.text}"。删除后将无法恢复,确定要删除吗?
-
-
-
- 取消
- {
- 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 : '未知错误'));
- });
- }}
- >
- 确认删除
-
-
-
-
- ) : (
-
- )}
-
-
- {item.text && (
-
{
- 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}
-
- )}
-
-
-
-
+ {data.map((item) => (
+
))}
@@ -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(false);
const [errorMessage, setErrorMessage] = useState('');
const [userInteracted, setUserInteracted] = useState(false);
const ref = useRef(null);
- const audioRecorderRef = useRef(null);
const initializingRef = useRef(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 = () => {
)}
- {lastRecognizedText && }
-
+ {showText && }
diff --git a/src/pages/muse/voice/modules/speak-db/speak.ts b/src/pages/muse/voice/modules/speak-db/speak.ts
index 11f6c03..8ef507c 100644
--- a/src/pages/muse/voice/modules/speak-db/speak.ts
+++ b/src/pages/muse/voice/modules/speak-db/speak.ts
@@ -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天中的第几天
diff --git a/src/pages/muse/voice/store/index.ts b/src/pages/muse/voice/store/index.ts
index 6ddb785..7559cad 100644
--- a/src/pages/muse/voice/store/index.ts
+++ b/src/pages/muse/voice/store/index.ts
@@ -1,3 +1,3 @@
// Store exports
-export { useVoiceStore, useVoiceList, useVoiceLoading, useVoiceError, cleanupVoiceUrls } from './voiceStore';
+export { useVoiceStore } from './voiceStore';
export type { VoiceState } from './voiceStore';
\ No newline at end of file
diff --git a/src/pages/muse/voice/store/relatime.ts b/src/pages/muse/voice/store/relatime.ts
deleted file mode 100644
index e5fee83..0000000
--- a/src/pages/muse/voice/store/relatime.ts
+++ /dev/null
@@ -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);
- }
-}
\ No newline at end of file
diff --git a/src/pages/muse/voice/store/resourceStore.ts b/src/pages/muse/voice/store/resourceStore.ts
new file mode 100644
index 0000000..7c6acea
--- /dev/null
+++ b/src/pages/muse/voice/store/resourceStore.ts
@@ -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;
+ mount: boolean;
+ upload: (speakData: Speak, blob?: Blob) => Promise;
+}
+export const useResourceStore = create((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);
+ }
+ }
+}));
\ No newline at end of file
diff --git a/src/pages/muse/voice/store/settingStore.ts b/src/pages/muse/voice/store/settingStore.ts
index e2b910b..f7af386 100644
--- a/src/pages/muse/voice/store/settingStore.ts
+++ b/src/pages/muse/voice/store/settingStore.ts
@@ -9,19 +9,13 @@ interface SettingState {
autoRecognize: boolean;
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()(
@@ -55,47 +31,21 @@ export const useSettingStore = create()(
// 初始状态 - 合并默认设置和从 localStorage 读取的火山引擎配置
isOpen: false,
...defaultSettings,
- ...getInitialVolcengineConfig(),
mount: false,
// 弹窗控制方法
openModal: () => set({ isOpen: true }),
closeModal: () => set({ isOpen: false }),
-
+
// 设置更新方法
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()(
)
);
-// 兼容原有 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);
- }
-};
diff --git a/src/pages/muse/voice/store/voiceStore.ts b/src/pages/muse/voice/store/voiceStore.ts
index ea04852..13980ec 100644
--- a/src/pages/muse/voice/store/voiceStore.ts
+++ b/src/pages/muse/voice/store/voiceStore.ts
@@ -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;
+ getList: () => Promise;
addVoice: (url: string, duration: number, audioBlob?: Blob) => Promise;
- updateVoice: (id: string, updates: Partial) => Promise;
- deleteVoice: (id: string) => Promise;
- recognizeVoice: (id: string) => Promise;
- clearTodayVoices: () => Promise;
- generateAudioUrls: () => Promise;
- refreshList: () => Promise;
+ deleteVoice: (voice: FlowmeSpeak) => Promise;
+ recognizeVoice: (voice: FlowmeSpeak) => Promise;
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()(
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);
-
- set({
- voiceList: todayVoices,
- currentDay: currentDay
- });
-
- // 为获取到的记录生成 audio URLs
- await generateAudioUrls();
-
+ const res = await flowmeApi.flowme.list({
+ type: 'muse',
+ pageSize: 99999,
+ })
+ console.log('Flowme 列出资源结果:', res);
+ if (res.code === 200) {
+ set({
+ voiceList: res.data.list || [],
+ currentDay: currentDay
+ });
+ }
} catch (error) {
console.error('初始化语音列表失败:', error);
setError(error instanceof Error ? error.message : '初始化失败');
@@ -115,10 +127,14 @@ export const useVoiceStore = create()(
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()(
// 创建语音记录(不保存 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()(
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()(
}
},
- // 更新语音记录
- updateVoice: async (id: string, updates: Partial) => {
- 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()(
},
// 识别语音记录
- 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,
+ }
+ }
+ }
+ })
+ 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) {
console.error('语音识别失败:', error);
setError(error instanceof Error ? error.message : '语音识别失败');
@@ -269,84 +238,27 @@ export const useVoiceStore = create()(
}
},
- // 清空今天的语音记录
- 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
@@ -355,24 +267,4 @@ export const useVoiceStore = create()(
);
// 导出类型以便其他地方使用
-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);
-}
\ No newline at end of file
+export type { VoiceState };
\ No newline at end of file
diff --git a/src/pages/page.tsx b/src/pages/page.tsx
index 9b174eb..0947042 100644
--- a/src/pages/page.tsx
+++ b/src/pages/page.tsx
@@ -1,3 +1,3 @@
-import { Record } from './muse/index';
+import { App } from './muse/voice/index';
-export default Record;
\ No newline at end of file
+export default App;
\ No newline at end of file