update
This commit is contained in:
148
pnpm-lock.yaml
generated
148
pnpm-lock.yaml
generated
@@ -90,6 +90,9 @@ importers:
|
||||
'@radix-ui/react-slot':
|
||||
specifier: ^1.2.4
|
||||
version: 1.2.4(@types/react@19.2.7)(react@19.2.3)
|
||||
'@radix-ui/react-tooltip':
|
||||
specifier: ^1.2.8
|
||||
version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@ricky0123/vad-web':
|
||||
specifier: ^0.0.30
|
||||
version: 0.0.30
|
||||
@@ -869,6 +872,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-arrow@1.1.7':
|
||||
resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-compose-refs@1.1.2':
|
||||
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
|
||||
peerDependencies:
|
||||
@@ -944,6 +960,19 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-popper@1.2.8':
|
||||
resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-portal@1.1.9':
|
||||
resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==}
|
||||
peerDependencies:
|
||||
@@ -1001,6 +1030,19 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-tooltip@1.2.8':
|
||||
resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-callback-ref@1.1.1':
|
||||
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
|
||||
peerDependencies:
|
||||
@@ -1046,6 +1088,40 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-rect@1.1.1':
|
||||
resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-size@1.1.1':
|
||||
resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-visually-hidden@1.2.3':
|
||||
resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/rect@1.1.1':
|
||||
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
||||
|
||||
'@ricky0123/vad-web@0.0.30':
|
||||
resolution: {integrity: sha512-cJyYrh4YeeUBJcbR9Bic/bFDyB9qBkAepvpuWM3vLxnAi7bC3VHzf51UeNdT+OtY4D7MLAgV8iJMc4z41ZnaWg==}
|
||||
|
||||
@@ -4419,6 +4495,15 @@ snapshots:
|
||||
'@types/react': 19.2.7
|
||||
'@types/react-dom': 19.2.3(@types/react@19.2.7)
|
||||
|
||||
'@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
dependencies:
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.7
|
||||
'@types/react-dom': 19.2.3(@types/react@19.2.7)
|
||||
|
||||
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.7)(react@19.2.3)':
|
||||
dependencies:
|
||||
react: 19.2.3
|
||||
@@ -4490,6 +4575,24 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.7
|
||||
|
||||
'@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
dependencies:
|
||||
'@floating-ui/react-dom': 2.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3)
|
||||
'@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.3)
|
||||
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.3)
|
||||
'@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.7)(react@19.2.3)
|
||||
'@radix-ui/react-use-size': 1.1.1(@types/react@19.2.7)(react@19.2.3)
|
||||
'@radix-ui/rect': 1.1.1
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.7
|
||||
'@types/react-dom': 19.2.3(@types/react@19.2.7)
|
||||
|
||||
'@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
dependencies:
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
@@ -4533,6 +4636,26 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.7
|
||||
|
||||
'@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3)
|
||||
'@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3)
|
||||
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.3)
|
||||
'@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.3)
|
||||
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.3)
|
||||
'@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.7
|
||||
'@types/react-dom': 19.2.3(@types/react@19.2.7)
|
||||
|
||||
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.7)(react@19.2.3)':
|
||||
dependencies:
|
||||
react: 19.2.3
|
||||
@@ -4567,6 +4690,31 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.7
|
||||
|
||||
'@radix-ui/react-use-rect@1.1.1(@types/react@19.2.7)(react@19.2.3)':
|
||||
dependencies:
|
||||
'@radix-ui/rect': 1.1.1
|
||||
react: 19.2.3
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.7
|
||||
|
||||
'@radix-ui/react-use-size@1.1.1(@types/react@19.2.7)(react@19.2.3)':
|
||||
dependencies:
|
||||
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.3)
|
||||
react: 19.2.3
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.7
|
||||
|
||||
'@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
dependencies:
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.7
|
||||
'@types/react-dom': 19.2.3(@types/react@19.2.7)
|
||||
|
||||
'@radix-ui/rect@1.1.1': {}
|
||||
|
||||
'@ricky0123/vad-web@0.0.30':
|
||||
dependencies:
|
||||
onnxruntime-web: 1.23.0
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@ricky0123/vad-web": "^0.0.30",
|
||||
"@szhsin/react-menu": "^4.5.1",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
|
||||
@@ -35,7 +35,9 @@ export class AudioRecorder {
|
||||
public onAudioData(callback: AudioDataCallback): void {
|
||||
this.onAudioDataCallback = callback;
|
||||
}
|
||||
|
||||
getMediaStream(): MediaStream | null {
|
||||
return this.mediaStream;
|
||||
}
|
||||
/**
|
||||
* 开始录制
|
||||
*/
|
||||
@@ -47,17 +49,17 @@ export class AudioRecorder {
|
||||
|
||||
try {
|
||||
// 获取麦克风权限
|
||||
this.mediaStream = await navigator.mediaDevices.getUserMedia({
|
||||
this.mediaStream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
autoGainControl: true,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 创建音频上下文
|
||||
this.audioContext = new AudioContext({
|
||||
sampleRate: this.config.sampleRate
|
||||
this.audioContext = new AudioContext({
|
||||
sampleRate: this.config.sampleRate
|
||||
});
|
||||
|
||||
// 加载AudioWorklet处理器
|
||||
@@ -68,7 +70,7 @@ export class AudioRecorder {
|
||||
|
||||
// 创建AudioWorklet节点
|
||||
this.workletNode = new AudioWorkletNode(
|
||||
this.audioContext,
|
||||
this.audioContext,
|
||||
'audio-recorder-processor',
|
||||
{
|
||||
processorOptions: {
|
||||
@@ -228,7 +230,7 @@ export class AudioRecorder {
|
||||
for (let i = 0; i < binary.length; i++) {
|
||||
binaryString += String.fromCharCode(binary[i]);
|
||||
}
|
||||
return typeof window !== 'undefined' && window.btoa
|
||||
return typeof window !== 'undefined' && window.btoa
|
||||
? window.btoa(binaryString)
|
||||
: Buffer.from(binaryString, 'binary').toString('base64');
|
||||
}
|
||||
|
||||
40
web/src/apps/muse/voice/modules/ShowText.tsx
Normal file
40
web/src/apps/muse/voice/modules/ShowText.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
Tooltip, TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Copy } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
|
||||
export const ShowText = (props: { text: string, icon?: any, title?: string }) => {
|
||||
const title = props.title || '';
|
||||
const icon = props.icon || '🕘';
|
||||
return (
|
||||
<div className="flex relative w-full overflow-hidden">
|
||||
<Tooltip >
|
||||
<TooltipTrigger className="text-left text-xs text-gray-400 mt-1 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{icon} {title ? `${title}:` : ''} {props.text}
|
||||
</TooltipTrigger>
|
||||
{props.text && <TooltipContent className="max-w-xs">
|
||||
{props.text}
|
||||
</TooltipContent>
|
||||
}
|
||||
</Tooltip>
|
||||
<div className="cursor-pointer shrink-0" onClick={() => {
|
||||
// copy
|
||||
navigator.clipboard.writeText(props.text).then(() => {
|
||||
toast.success('已复制', {
|
||||
autoClose: 500,
|
||||
position: 'top-center'
|
||||
});
|
||||
}).catch((error) => {
|
||||
console.error('复制失败:', error);
|
||||
toast.error('复制失败,请手动选择文字复制');
|
||||
});
|
||||
}}>
|
||||
<Copy className='text-gray-400' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { Speak } from "./speak-db/speak";
|
||||
import { useVoiceStore } from "../store/voiceStore";
|
||||
import { useSettingStore } from "../store/settingStore";
|
||||
import { SettingModal } from "./SettingModal";
|
||||
|
||||
import { AudioRecorder } from "./AudioRecorder";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "../../../../components/ui/alert-dialog";
|
||||
import { ShowText } from "./ShowText";
|
||||
|
||||
type VadVoiceProps = {
|
||||
data: Speak;
|
||||
@@ -381,6 +382,7 @@ export const VadVoice = () => {
|
||||
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||
const [userInteracted, setUserInteracted] = useState<boolean>(false);
|
||||
const ref = useRef<MicVAD | null>(null);
|
||||
const audioRecorderRef = useRef<AudioRecorder | null>(null);
|
||||
const initializingRef = useRef<boolean>(false);
|
||||
|
||||
async function initializeVAD(ls: boolean = true) {
|
||||
@@ -397,11 +399,26 @@ 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));
|
||||
relatime?.sendBase64?.(utils.arrayBufferToBase64(wavBuffer), { isRelatime: false });
|
||||
const audioBlob = new Blob([wavBuffer], { type: 'audio/wav' })
|
||||
const tempUrl = URL.createObjectURL(audioBlob)
|
||||
|
||||
@@ -474,8 +491,13 @@ export const VadVoice = () => {
|
||||
document.removeEventListener('click', handleFirstClick);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', handleFirstClick);
|
||||
// 如果是tauri的环境,不需要用户交互
|
||||
// @ts-ignore
|
||||
if (window.__TAURI_INTERNALS__) {
|
||||
handleUserInteraction();
|
||||
} else {
|
||||
document.addEventListener('click', handleFirstClick);
|
||||
}
|
||||
return () => {
|
||||
document.removeEventListener('click', handleFirstClick);
|
||||
};
|
||||
@@ -499,6 +521,9 @@ export const VadVoice = () => {
|
||||
ref.current = null;
|
||||
setListen(false);
|
||||
setVadStatus('idle');
|
||||
audioRecorderRef.current?.stop();
|
||||
audioRecorderRef.current = null;
|
||||
console.log('VAD closed');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,10 +572,10 @@ export const VadVoice = () => {
|
||||
</div>
|
||||
|
||||
{/* Voice Control Bottom Section */}
|
||||
<div className="border-t border-gray-200 p-3 bg-gray-50">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="relative">
|
||||
<div className="border-t border-gray-200 p-3 bg-gray-50 w-full overflow-hidden">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center space-x-3 min-w-0 flex-1">
|
||||
<div className="relative shrink-0">
|
||||
<div className={clsx(
|
||||
"h-8 w-8 rounded-lg bg-gradient-to-l from-[#7928CA] to-[#008080] flex items-center justify-center",
|
||||
{ "animate-pulse": listen, "low-energy-spin": listen }
|
||||
@@ -565,7 +590,7 @@ export const VadVoice = () => {
|
||||
<div className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full animate-pulse"></div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="w-[90%]">
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{vadStatus === 'initializing' || storeLoading ? 'Initializing...' :
|
||||
vadStatus === 'error' || storeError ? 'Error' :
|
||||
@@ -578,53 +603,13 @@ export const VadVoice = () => {
|
||||
`${voiceList.length} recording${voiceList.length !== 1 ? 's' : ''}`
|
||||
)}
|
||||
</div>
|
||||
<div className=" ">
|
||||
{lastRecognizedText && (
|
||||
<div className="flex">
|
||||
<div className="text-xs text-gray-400 mt-1 truncate">
|
||||
🕘 上次识别: {lastRecognizedText}
|
||||
</div>
|
||||
<div className="cursor-pointer " onClick={() => {
|
||||
// copy
|
||||
navigator.clipboard.writeText(lastRecognizedText).then(() => {
|
||||
toast.success('已复制', {
|
||||
autoClose: 500,
|
||||
position: 'top-center'
|
||||
});
|
||||
}).catch((error) => {
|
||||
console.error('复制失败:', error);
|
||||
toast.error('复制失败,请手动选择文字复制');
|
||||
});
|
||||
}}>
|
||||
<Copy className='text-gray-400' />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{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 className="w-full">
|
||||
{lastRecognizedText && <ShowText text={lastRecognizedText} title="上次识别" icon={'🕘'} />}
|
||||
<ShowText text={showText} icon={'📝'} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center space-x-2 shrink-0">
|
||||
{vadStatus === 'error' && (
|
||||
<button
|
||||
onClick={retryInitialization}
|
||||
@@ -637,7 +622,7 @@ export const VadVoice = () => {
|
||||
onClick={handleStartStop}
|
||||
disabled={vadStatus === 'initializing'}
|
||||
className={clsx(
|
||||
"w-8 h-8 text-xs font-medium rounded-full flex items-center justify-center transition-colors cursor-pointer",
|
||||
"md:flex w-8 h-8 text-xs font-medium rounded-full items-center justify-center transition-colors cursor-pointer",
|
||||
vadStatus === 'initializing' && "opacity-50 cursor-not-allowed",
|
||||
listen
|
||||
? "bg-red-100 text-red-700 hover:bg-red-200"
|
||||
@@ -653,7 +638,7 @@ export const VadVoice = () => {
|
||||
setAutoRecognize(newStatus);
|
||||
}}
|
||||
className={clsx(
|
||||
"w-8 h-8 hover:bg-gray-200 rounded-full flex items-center justify-center text-gray-700 transition-colors cursor-pointer",
|
||||
"hidden md:flex w-8 h-8 hover:bg-gray-200 rounded-full items-center justify-center text-gray-700 transition-colors cursor-pointer",
|
||||
{ "bg-blue-200": autoRecognize }
|
||||
)}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ export class Relatime {
|
||||
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)
|
||||
@@ -50,9 +51,17 @@ export class Relatime {
|
||||
const voice = data.toString('base64');
|
||||
this.asr.ws.send(JSON.stringify({ voice }));
|
||||
}
|
||||
sendBase64(data: string, opts?: { isRelatime?: boolean }) {
|
||||
setIsRelatime(isRelatime: boolean) {
|
||||
this.isRelatime = isRelatime;
|
||||
}
|
||||
async sendBase64(data: string, opts?: { isRelatime?: boolean }) {
|
||||
if (!this.ready) return;
|
||||
console.log('send 花费时间:', Date.now() - this.startTime);
|
||||
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);
|
||||
|
||||
59
web/src/components/ui/tooltip.tsx
Normal file
59
web/src/components/ui/tooltip.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
Reference in New Issue
Block a user