feat: update dependencies and add video-tools package
- Added "@kevisual/video-tools" dependency to server/package.json. - Updated VITE_API_URL in astro.config.mjs to point to new local server. - Modified proxy settings in astro.config.mjs to include WebSocket support. - Updated various dependencies in web/package.json for improved functionality and security. - Refactored MuseApp component to use new resizable panel components. - Implemented real-time text display and copy functionality in VadVoice component. - Created a new Relatime class for handling WebSocket connections and real-time updates. - Added new board and muse pages to render the Record component.
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -4,8 +4,4 @@ dist
|
|||||||
|
|
||||||
.turbo
|
.turbo
|
||||||
|
|
||||||
filebrowser
|
|
||||||
|
|
||||||
filebrowser.db
|
|
||||||
|
|
||||||
/data
|
/data
|
||||||
@@ -15,9 +15,9 @@
|
|||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"packageManager": "pnpm@10.18.3",
|
"packageManager": "pnpm@10.26.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"turbo": "^2.5.8"
|
"turbo": "^2.7.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
2071
pnpm-lock.yaml
generated
2071
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,7 @@
|
|||||||
"@kevisual/noco": "^0.0.1",
|
"@kevisual/noco": "^0.0.1",
|
||||||
"@kevisual/query": "^0.0.29",
|
"@kevisual/query": "^0.0.29",
|
||||||
"@kevisual/router": "^0.0.29",
|
"@kevisual/router": "^0.0.29",
|
||||||
|
"@kevisual/video-tools": "^0.0.10",
|
||||||
"fast-glob": "^3.3.3",
|
"fast-glob": "^3.3.3",
|
||||||
"pocketbase": "^0.26.2",
|
"pocketbase": "^0.26.2",
|
||||||
"unstorage": "^1.17.1"
|
"unstorage": "^1.17.1"
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import tailwindcss from '@tailwindcss/vite';
|
|||||||
|
|
||||||
const isDev = process.env.NODE_ENV === 'development';
|
const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
let target = process.env.VITE_API_URL || 'http://localhost:4005';
|
let target = process.env.VITE_API_URL || 'http://localhost:51015';
|
||||||
const apiProxy = { target: target, changeOrigin: true, ws: true, rewriteWsOrigin: true, secure: false, cookieDomainRewrite: 'localhost' };
|
const apiProxy = { target: target, changeOrigin: true, ws: true, rewriteWsOrigin: true, secure: false, cookieDomainRewrite: 'localhost' };
|
||||||
|
|
||||||
let proxy = {
|
let proxy = {
|
||||||
'/root/': {
|
'/root': apiProxy,
|
||||||
target: `${target}/root/`,
|
|
||||||
},
|
|
||||||
'/api': apiProxy,
|
'/api': apiProxy,
|
||||||
|
'/ws': apiProxy,
|
||||||
};
|
};
|
||||||
|
|
||||||
const basename = isDev ? undefined : `${pkgs.basename}`;
|
const basename = isDev ? undefined : `${pkgs.basename}`;
|
||||||
@@ -25,15 +25,18 @@ export default defineConfig({
|
|||||||
// sitemap(), // sitemap must be site has a domain
|
// sitemap(), // sitemap must be site has a domain
|
||||||
],
|
],
|
||||||
|
|
||||||
|
server: {
|
||||||
|
port: 4321,
|
||||||
|
host: '0.0.0.0',
|
||||||
|
allowedHosts: true,
|
||||||
|
},
|
||||||
|
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [tailwindcss()],
|
plugins: [tailwindcss()],
|
||||||
define: {
|
define: {
|
||||||
basename: JSON.stringify(basename || ''),
|
basename: JSON.stringify(basename || ''),
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 7008,
|
|
||||||
host: '0.0.0.0',
|
|
||||||
allowedHosts: true,
|
|
||||||
proxy,
|
proxy,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,49 +17,50 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/mdx": "^4.3.7",
|
"@astrojs/mdx": "^4.3.13",
|
||||||
"@astrojs/react": "^4.4.0",
|
"@astrojs/react": "^4.4.2",
|
||||||
"@astrojs/sitemap": "^3.6.0",
|
"@astrojs/sitemap": "^3.6.0",
|
||||||
"@faker-js/faker": "^10.1.0",
|
"@faker-js/faker": "^10.1.0",
|
||||||
"@floating-ui/react": "^0.27.16",
|
"@floating-ui/react": "^0.27.16",
|
||||||
"@kevisual/noco": "^0.0.1",
|
"@kevisual/noco": "^0.0.10",
|
||||||
"@kevisual/query": "^0.0.29",
|
"@kevisual/query": "^0.0.33",
|
||||||
"@kevisual/query-login": "^0.0.6",
|
"@kevisual/query-login": "^0.0.7",
|
||||||
"@kevisual/registry": "^0.0.1",
|
"@kevisual/registry": "^0.0.1",
|
||||||
|
"@kevisual/video-tools": "^0.0.11",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@ricky0123/vad-web": "^0.0.28",
|
"@ricky0123/vad-web": "^0.0.30",
|
||||||
"@szhsin/react-menu": "^4.5.0",
|
"@szhsin/react-menu": "^4.5.1",
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@tanstack/react-form": "^1.23.7",
|
"@tanstack/react-form": "^1.27.5",
|
||||||
"@tanstack/react-query": "^5.90.5",
|
"@tanstack/react-query": "^5.90.12",
|
||||||
"astro": "^5.14.4",
|
"astro": "^5.16.6",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.18",
|
"dayjs": "^1.11.19",
|
||||||
"es-toolkit": "^1.40.0",
|
"es-toolkit": "^1.43.0",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"graphology": "^0.26.0",
|
"graphology": "^0.26.0",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.22",
|
||||||
"lucide-react": "^0.545.0",
|
"lucide-react": "^0.562.0",
|
||||||
"marked": "^16.4.1",
|
"marked": "^17.0.1",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
"pocketbase": "^0.26.2",
|
"pocketbase": "^0.26.5",
|
||||||
"pouchdb-adapter-memory": "^9.0.0",
|
"pouchdb-adapter-memory": "^9.0.0",
|
||||||
"pouchdb-browser": "^9.0.0",
|
"pouchdb-browser": "^9.0.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.3",
|
||||||
"react-modal": "^3.16.3",
|
"react-modal": "^3.16.3",
|
||||||
"react-resizable-panels": "^3.0.6",
|
"react-resizable-panels": "^4.0.13",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
"react-virtualized": "^9.22.6",
|
"react-virtualized": "^9.22.6",
|
||||||
"sigma": "^3.0.2",
|
"sigma": "^3.0.2",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.4.0",
|
||||||
"three": "^0.180.0",
|
"three": "^0.182.0",
|
||||||
"wavesurfer.js": "^7.11.0",
|
"wavesurfer.js": "^7.12.1",
|
||||||
"zustand": "^5.0.8"
|
"zustand": "^5.0.9"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
@@ -68,18 +69,18 @@
|
|||||||
"@kevisual/types": "^0.0.10",
|
"@kevisual/types": "^0.0.10",
|
||||||
"@types/pouchdb": "^6.4.2",
|
"@types/pouchdb": "^6.4.2",
|
||||||
"@types/pouchdb-browser": "^6.1.5",
|
"@types/pouchdb-browser": "^6.1.5",
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.2",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/react-modal": "^3.16.3",
|
"@types/react-modal": "^3.16.3",
|
||||||
"@types/react-virtualized": "^9.22.3",
|
"@types/react-virtualized": "^9.22.3",
|
||||||
"@types/three": "^0.180.0",
|
"@types/three": "^0.182.0",
|
||||||
"@vitejs/plugin-basic-ssl": "^2.1.0",
|
"@vitejs/plugin-basic-ssl": "^2.1.0",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"pouchdb": "^9.0.0",
|
"pouchdb": "^9.0.0",
|
||||||
"tailwindcss": "^4.1.14",
|
"tailwindcss": "^4.1.18",
|
||||||
"tw-animate-css": "^1.4.0"
|
"tw-animate-css": "^1.4.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.18.3",
|
"packageManager": "pnpm@10.26.1",
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"@tailwindcss/oxide",
|
"@tailwindcss/oxide",
|
||||||
"esbuild",
|
"esbuild",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ToastContainer, toast } from 'react-toastify';
|
import { ToastContainer, toast } from 'react-toastify';
|
||||||
import { AuthProvider } from '../login/AuthProvider';
|
import { AuthProvider } from '../login/AuthProvider';
|
||||||
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
import { Panel, Group, Separator } from 'react-resizable-panels';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
|
|
||||||
import { App as Voice } from './voice/index.tsx';
|
import { App as Voice } from './voice/index.tsx';
|
||||||
@@ -180,21 +180,21 @@ export const MuseApp = () => {
|
|||||||
|
|
||||||
{/* Resizable Panels */}
|
{/* Resizable Panels */}
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
<PanelGroup direction="horizontal">
|
<Group orientation="horizontal">
|
||||||
{showLeftPanel && <LeftPanel />}
|
{showLeftPanel && <LeftPanel />}
|
||||||
|
|
||||||
{showLeftPanel && showCenterPanel && (
|
{showLeftPanel && showCenterPanel && (
|
||||||
<PanelResizeHandle className="w-1 bg-gray-300 hover:bg-gray-400 cursor-col-resize transition-colors" />
|
<Separator className="w-1 bg-gray-300 hover:bg-gray-400 cursor-col-resize transition-colors" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showCenterPanel && <CenterPanel />}
|
{showCenterPanel && <CenterPanel />}
|
||||||
|
|
||||||
{showCenterPanel && showRightPanel && (
|
{showCenterPanel && showRightPanel && (
|
||||||
<PanelResizeHandle className="w-1 bg-gray-300 hover:bg-gray-400 cursor-col-resize transition-colors" />
|
<Separator className="w-1 bg-gray-300 hover:bg-gray-400 cursor-col-resize transition-colors" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showRightPanel && <RightPanel isVisible={showRightPanel} />}
|
{showRightPanel && <RightPanel isVisible={showRightPanel} />}
|
||||||
</PanelGroup>
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -220,3 +220,23 @@ export const App: React.FC = () => {
|
|||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Record: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<AuthProvider>
|
||||||
|
<Voice />
|
||||||
|
<ToastContainer
|
||||||
|
position="top-right"
|
||||||
|
autoClose={1000}
|
||||||
|
hideProgressBar={false}
|
||||||
|
newestOnTop={false}
|
||||||
|
closeOnClick
|
||||||
|
rtl={false}
|
||||||
|
pauseOnFocusLoss
|
||||||
|
draggable
|
||||||
|
pauseOnHover
|
||||||
|
theme="light"
|
||||||
|
/>
|
||||||
|
</AuthProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ 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 } from "lucide-react";
|
import { MoreHorizontal, Play, Pause, Settings, FileAudio, StopCircle, Loader, Copy } 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 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
@@ -223,6 +223,12 @@ const VoicePlayer = ({ data }: VadVoiceProps) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
export const ShowVoicePlayer = ({ data }: { data: Speak[] }) => {
|
export const ShowVoicePlayer = ({ data }: { data: Speak[] }) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const bottomElement = document.getElementById('voice-list-bottom');
|
||||||
|
if (bottomElement) {
|
||||||
|
bottomElement.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
}, [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, index) => (
|
||||||
<li key={index} className="bg-white rounded-lg border border-gray-200 p-2 hover:shadow-sm transition-shadow">
|
<li key={index} className="bg-white rounded-lg border border-gray-200 p-2 hover:shadow-sm transition-shadow">
|
||||||
@@ -343,6 +349,8 @@ export const ShowVoicePlayer = ({ data }: { data: Speak[] }) => {
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
<div id="voice-list-bottom" />
|
||||||
</ul>)
|
</ul>)
|
||||||
}
|
}
|
||||||
export const VadVoice = () => {
|
export const VadVoice = () => {
|
||||||
@@ -353,9 +361,11 @@ export const VadVoice = () => {
|
|||||||
error: storeError,
|
error: storeError,
|
||||||
initialize: initializeStore,
|
initialize: initializeStore,
|
||||||
addVoice,
|
addVoice,
|
||||||
setError: setStoreError
|
setError: setStoreError,
|
||||||
|
relatimeParialText,
|
||||||
|
relatimeFinalText
|
||||||
} = useVoiceStore();
|
} = useVoiceStore();
|
||||||
|
const showText = relatimeFinalText || relatimeParialText;
|
||||||
// 使用设置 store
|
// 使用设置 store
|
||||||
const {
|
const {
|
||||||
openModal: openSettingModal,
|
openModal: openSettingModal,
|
||||||
@@ -391,7 +401,8 @@ export const VadVoice = () => {
|
|||||||
const wavBuffer = utils.encodeWAV(audio)
|
const wavBuffer = utils.encodeWAV(audio)
|
||||||
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)
|
||||||
|
const relatime = useVoiceStore.getState().relatime;
|
||||||
|
relatime?.sendBase64?.(utils.arrayBufferToBase64(wavBuffer));
|
||||||
// 从实际音频文件获取准确时长
|
// 从实际音频文件获取准确时长
|
||||||
const getDuration = (): Promise<number> => {
|
const getDuration = (): Promise<number> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@@ -508,7 +519,7 @@ export const VadVoice = () => {
|
|||||||
};
|
};
|
||||||
return <div className="h-full flex flex-col">
|
return <div className="h-full flex flex-col">
|
||||||
{/* Audio Recordings List */}
|
{/* Audio Recordings List */}
|
||||||
<div className="flex-1 overflow-y-auto px-2 py-3 min-h-0 max-h-200">
|
<div className="flex-1 overflow-y-auto px-2 py-3 min-h-0 max-h-76 overflow-hidden">
|
||||||
{
|
{
|
||||||
voiceList.length === 0 ? (
|
voiceList.length === 0 ? (
|
||||||
<div className="text-center text-gray-400 text-sm py-8">
|
<div className="text-center text-gray-400 text-sm py-8">
|
||||||
@@ -564,6 +575,29 @@ export const VadVoice = () => {
|
|||||||
`${voiceList.length} recording${voiceList.length !== 1 ? 's' : ''}`
|
`${voiceList.length} recording${voiceList.length !== 1 ? 's' : ''}`
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className=" ">
|
||||||
|
{showText && (
|
||||||
|
<div className="flex">
|
||||||
|
<div className="text-xs text-gray-600 mt-1 truncate">
|
||||||
|
📝 {showText}
|
||||||
|
</div>
|
||||||
|
<div className="cursor-pointer" onClick={() => {
|
||||||
|
// copy
|
||||||
|
navigator.clipboard.writeText(showText).then(() => {
|
||||||
|
toast.success('已复制', {
|
||||||
|
autoClose: 500,
|
||||||
|
position: 'top-center'
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('复制失败:', error);
|
||||||
|
toast.error('复制失败,请手动选择文字复制');
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
<Copy />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
@@ -572,7 +606,7 @@ export const VadVoice = () => {
|
|||||||
onClick={retryInitialization}
|
onClick={retryInitialization}
|
||||||
className="px-2 py-1 text-xs font-medium text-blue-700 bg-blue-100 hover:bg-blue-200 rounded-md transition-colors"
|
className="px-2 py-1 text-xs font-medium text-blue-700 bg-blue-100 hover:bg-blue-200 rounded-md transition-colors"
|
||||||
>
|
>
|
||||||
Retry
|
重试
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,26 +1,10 @@
|
|||||||
// Speak 数据库和服务的统一导出文件
|
// Speak 数据库和服务的统一导出文件
|
||||||
|
|
||||||
// 类型定义
|
// 类型定义
|
||||||
export {
|
export * from './speak.ts';
|
||||||
Speak,
|
|
||||||
SpeakType,
|
|
||||||
CreateSpeakData,
|
|
||||||
UpdateSpeakData,
|
|
||||||
getDayOfYear
|
|
||||||
} from './speak';
|
|
||||||
|
|
||||||
// 数据库操作
|
// 数据库操作
|
||||||
export {
|
export * from './speak-db';
|
||||||
SpeakDB,
|
|
||||||
SpeakDocument,
|
|
||||||
speakDB,
|
|
||||||
initSpeakDB,
|
|
||||||
createSpeakDB
|
|
||||||
} from './speak-db';
|
|
||||||
|
|
||||||
// 服务层
|
// 服务层
|
||||||
export {
|
export * from './speak-service';
|
||||||
SpeakService,
|
|
||||||
speakService,
|
|
||||||
exampleUsage
|
|
||||||
} from './speak-service';
|
|
||||||
65
web/src/apps/muse/voice/store/relatime.ts
Normal file
65
web/src/apps/muse/voice/store/relatime.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
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
|
||||||
|
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 }));
|
||||||
|
}
|
||||||
|
sendBase64(data: string) {
|
||||||
|
if (!this.ready) return;
|
||||||
|
this.asr.ws.send(JSON.stringify({ voice: data, format: 'float32' }));
|
||||||
|
if (this.timeoutHandle) {
|
||||||
|
clearTimeout(this.timeoutHandle);
|
||||||
|
}
|
||||||
|
this.timeoutHandle = setTimeout(() => {
|
||||||
|
this.asr.sendBlankJson()
|
||||||
|
this.timeoutHandle = null;
|
||||||
|
}, 10000); // 5秒钟没有数据则发送空JSON保持连接
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { Speak, getDayOfYear, CreateSpeakData } from '../modules/speak-db/speak'
|
|||||||
import { speakService } from '../modules/speak-db/speak-service';
|
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';
|
||||||
|
|
||||||
interface VoiceState {
|
interface VoiceState {
|
||||||
// 状态数据
|
// 状态数据
|
||||||
@@ -23,19 +24,29 @@ interface VoiceState {
|
|||||||
refreshList: () => 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;
|
||||||
|
relatimeParialText: string;
|
||||||
|
relatimeFinalText: string;
|
||||||
|
setRelatimeParialText: (text: string) => void;
|
||||||
|
setRelatimeFinalText: (text: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助函数:将 Blob 转换为 base64 字符串
|
// 辅助函数:将 Blob 转换为 base64 字符串(兼容 Node.js)
|
||||||
const blobToBase64 = (blob: Blob): Promise<string> => {
|
const blobToBase64 = (blob: Blob): Promise<string> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = () => {
|
reader.onload = () => {
|
||||||
const result = reader.result as string;
|
const arrayBuffer = reader.result as ArrayBuffer;
|
||||||
// 移除 data URL 前缀,只保留 base64 数据
|
// 转换为 Uint8Array 再转为 base64,与 Node.js Buffer.toString('base64') 一致
|
||||||
resolve(result.split(',')[1] || result);
|
const bytes = new Uint8Array(arrayBuffer);
|
||||||
|
let binary = '';
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
resolve(window.btoa(binary));
|
||||||
};
|
};
|
||||||
reader.onerror = reject;
|
reader.onerror = reject;
|
||||||
reader.readAsDataURL(blob);
|
reader.readAsArrayBuffer(blob); // 使用 ArrayBuffer 而不是 DataURL
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -75,7 +86,8 @@ export const useVoiceStore = create<VoiceState>()(
|
|||||||
// 初始化:从 IndexedDB 获取当天的记录
|
// 初始化:从 IndexedDB 获取当天的记录
|
||||||
initialize: async () => {
|
initialize: async () => {
|
||||||
const { setLoading, setError, generateAudioUrls } = get();
|
const { setLoading, setError, generateAudioUrls } = get();
|
||||||
|
const relatime = new Relatime();
|
||||||
|
set({ relatime });
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -130,7 +142,6 @@ export const useVoiceStore = create<VoiceState>()(
|
|||||||
if (autoRecognize) {
|
if (autoRecognize) {
|
||||||
speakData.text = await getText(fileData || '').then(res => res.text);
|
speakData.text = await getText(fileData || '').then(res => res.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存到 IndexedDB(不包含 url)
|
// 保存到 IndexedDB(不包含 url)
|
||||||
const newSpeak = await speakService.createSpeakAuto(speakData);
|
const newSpeak = await speakService.createSpeakAuto(speakData);
|
||||||
|
|
||||||
@@ -323,7 +334,16 @@ export const useVoiceStore = create<VoiceState>()(
|
|||||||
// 设置加载状态
|
// 设置加载状态
|
||||||
setLoading: (loading: boolean) => {
|
setLoading: (loading: boolean) => {
|
||||||
set({ isLoading: loading });
|
set({ isLoading: loading });
|
||||||
}
|
},
|
||||||
|
|
||||||
|
relatimeFinalText: '',
|
||||||
|
setRelatimeFinalText: (text: string) => {
|
||||||
|
set({ relatimeFinalText: text, relatimeParialText: '' });
|
||||||
|
},
|
||||||
|
setRelatimeParialText: (text: string) => {
|
||||||
|
set({ relatimeFinalText: '', relatimeParialText: text });
|
||||||
|
},
|
||||||
|
relatimeParialText: '',
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'voice-store', // persist key
|
name: 'voice-store', // persist key
|
||||||
|
|||||||
8
web/src/pages/board.astro
Normal file
8
web/src/pages/board.astro
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
import Html from '../components/html.astro';
|
||||||
|
import { App } from '@/apps/muse/index.tsx';
|
||||||
|
---
|
||||||
|
|
||||||
|
<Html>
|
||||||
|
<App client:only />
|
||||||
|
</Html>
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
import Html from '../components/html.astro';
|
import Html from '../components/html.astro';
|
||||||
import { App } from '@/apps/muse/index.tsx';
|
import { Record } from '@/apps/muse/index.tsx';
|
||||||
---
|
---
|
||||||
|
|
||||||
<Html>
|
<Html>
|
||||||
<App client:only />
|
<Record client:only />
|
||||||
</Html>
|
</Html>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
import Html from '../components/html.astro';
|
import Html from '../components/html.astro';
|
||||||
import { App } from '@/apps/muse/index.tsx';
|
import { Record } from '@/apps/muse/index.tsx';
|
||||||
---
|
---
|
||||||
|
|
||||||
<Html>
|
<Html>
|
||||||
<App client:only />
|
<Record client:only />
|
||||||
</Html>
|
</Html>
|
||||||
|
|||||||
Reference in New Issue
Block a user