Files
CamTalk/frontend/src/components/EdgeProcessor/index.tsx

122 lines
3.4 KiB
TypeScript
Raw Normal View History

// ============================================================
// EdgeProcessor — 边缘预处理VAD + 关键帧检测)
// 职责:浏览器端语音活动检测、关键帧筛选
// 技术:@ricky0123/vad-webVAD、ONNX Runtime Web关键帧检测
// ============================================================
import { useCallback, useEffect, useRef, useState } from "react";
import { MicVAD } from "@ricky0123/vad-web";
export interface VADOptions {
/** 语音结束回调,携带录音 Float32Array16kHz */
onSpeechEnd?: (audio: Float32Array) => void;
/** 语音开始回调 */
onSpeechStart?: () => void;
/** 语音过短被忽略回调 */
onVADMisfire?: () => void;
}
/**
* Hook
* @ricky0123/vad-web MicVAD
*/
export function useVAD(options?: VADOptions) {
const [isSpeaking, setIsSpeaking] = useState(false);
const [isReady, setIsReady] = useState(false);
const [error, setError] = useState<string | null>(null);
const vadRef = useRef<MicVAD | null>(null);
const optionsRef = useRef(options);
// 保持 options 引用最新,避免回调闭包问题
useEffect(() => {
optionsRef.current = options;
}, [options]);
/**
* VAD
* @param stream MediaStream
*/
const start = useCallback(async (stream: MediaStream) => {
// 如果已有实例,先销毁
if (vadRef.current) {
await vadRef.current.destroy();
vadRef.current = null;
}
try {
const vad = await MicVAD.new({
getStream: () => Promise.resolve(stream),
startOnLoad: true,
model: "legacy",
onSpeechStart: () => {
setIsSpeaking(true);
optionsRef.current?.onSpeechStart?.();
},
onSpeechEnd: (audio: Float32Array) => {
setIsSpeaking(false);
optionsRef.current?.onSpeechEnd?.(audio);
},
onVADMisfire: () => {
setIsSpeaking(false);
optionsRef.current?.onVADMisfire?.();
},
// VAD 参数(对齐 docs/06-语音交互.md 推荐值)
positiveSpeechThreshold: 0.5,
negativeSpeechThreshold: 0.35,
redemptionMs: 300,
preSpeechPadMs: 300,
minSpeechMs: 250,
submitUserSpeechOnPause: false,
});
vadRef.current = vad;
setIsReady(true);
setError(null);
} catch (err) {
const message = err instanceof Error ? err.message : "VAD 初始化失败";
setError(message);
console.error("[VAD] 初始化失败:", err);
}
}, []);
/** 停止 VAD 并销毁实例 */
const stop = useCallback(async () => {
if (vadRef.current) {
await vadRef.current.destroy();
vadRef.current = null;
}
setIsReady(false);
setIsSpeaking(false);
}, []);
// 组件卸载时清理
useEffect(() => {
return () => {
vadRef.current?.destroy();
};
}, []);
return { isSpeaking, isReady, error, start, stop };
}
// ---- 关键帧检测ONNX Runtime Web----
export function useKeyframeDetection() {
// TODO: 加载 ONNX 模型后设为 true
const isReady = false;
const isKeyframe = useCallback(
(_currentFrame: ImageData, _previousFrame: ImageData): boolean => {
// TODO: 实现像素差异对比
return true; // 暂时所有帧都视为关键帧
},
[],
);
return { isReady, isKeyframe };
}