// ============================================================ // EdgeProcessor — 边缘预处理(VAD + 关键帧检测) // 职责:浏览器端语音活动检测、关键帧筛选 // 技术:@ricky0123/vad-web(VAD)、ONNX Runtime Web(关键帧检测) // ============================================================ import { useCallback, useEffect, useRef, useState } from "react"; import { MicVAD } from "@ricky0123/vad-web"; export interface VADOptions { /** 语音结束回调,携带录音 Float32Array(16kHz) */ 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(null); const vadRef = useRef(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 }; }